Merge branch 'frappe:develop' into develop
diff --git a/.github/helper/install.sh b/.github/helper/install.sh
index 69749c9..f0f83b0 100644
--- a/.github/helper/install.sh
+++ b/.github/helper/install.sh
@@ -11,7 +11,7 @@
 
 cd ~ || exit
 
-sudo apt-get install redis-server libcups2-dev
+sudo apt update && sudo apt install redis-server libcups2-dev
 
 pip install frappe-bench
 
diff --git a/.github/stale.yml b/.github/stale.yml
index fbf6447..da15d32 100644
--- a/.github/stale.yml
+++ b/.github/stale.yml
@@ -24,14 +24,4 @@
     :) Also, even if it is closed, you can always reopen the PR when you're
     ready. Thank you for contributing.
 
-issues:
-  daysUntilStale: 90
-  daysUntilClose: 7
-  exemptLabels:
-    - valid
-    - to-validate
-    - QA
-  markComment: >
-    This issue has been automatically marked as inactive because it has not had
-    recent activity and it wasn't validated by maintainer team. It will be
-    closed within a week if no further activity occurs.
+only: pulls
diff --git a/cypress.json b/cypress.json
deleted file mode 100644
index 02b10d8..0000000
--- a/cypress.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
-  "baseUrl": "http://test_site:8000/",
-  "projectId": "da59y9",
-  "adminPassword": "admin",
-  "defaultCommandTimeout": 20000,
-  "pageLoadTimeout": 15000,
-  "retries": {
-    "runMode": 2,
-    "openMode": 2
-  }
-}
diff --git a/cypress/fixtures/example.json b/cypress/fixtures/example.json
deleted file mode 100644
index da18d93..0000000
--- a/cypress/fixtures/example.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
-  "name": "Using fixtures to represent data",
-  "email": "hello@cypress.io",
-  "body": "Fixtures are a great way to mock data for responses to routes"
-}
\ No newline at end of file
diff --git a/cypress/integration/test_bulk_transaction_processing.js b/cypress/integration/test_bulk_transaction_processing.js
deleted file mode 100644
index 428ec51..0000000
--- a/cypress/integration/test_bulk_transaction_processing.js
+++ /dev/null
@@ -1,44 +0,0 @@
-describe("Bulk Transaction Processing", () => {
-	before(() => {
-		cy.login();
-		cy.visit("/app/website");
-	});
-
-	it("Creates To Sales Order", () => {
-		cy.visit("/app/sales-order");
-		cy.url().should("include", "/sales-order");
-		cy.window()
-			.its("frappe.csrf_token")
-			.then((csrf_token) => {
-				return cy
-					.request({
-						url: "/api/method/erpnext.tests.ui_test_bulk_transaction_processing.create_records",
-						method: "POST",
-						headers: {
-							Accept: "application/json",
-							"Content-Type": "application/json",
-							"X-Frappe-CSRF-Token": csrf_token,
-						},
-						timeout: 60000,
-					})
-					.then((res) => {
-						expect(res.status).eq(200);
-					});
-			});
-		cy.wait(5000);
-		cy.get(
-			".list-row-head > .list-header-subject > .list-row-col > .list-check-all"
-		).check({ force: true });
-		cy.wait(3000);
-		cy.get(".actions-btn-group > .btn-primary").click({ force: true });
-		cy.wait(3000);
-		cy.get(".dropdown-menu-right > .user-action > .dropdown-item")
-			.contains("Sales Invoice")
-			.click({ force: true });
-		cy.wait(3000);
-		cy.get(".modal-content > .modal-footer > .standard-actions")
-			.contains("Yes")
-			.click({ force: true });
-		cy.contains("Creation of Sales Invoice successful");
-	});
-});
diff --git a/cypress/integration/test_customer.js b/cypress/integration/test_customer.js
deleted file mode 100644
index 3d6ed5d..0000000
--- a/cypress/integration/test_customer.js
+++ /dev/null
@@ -1,13 +0,0 @@
-
-context('Customer', () => {
-	before(() => {
-		cy.login();
-	});
-	it('Check Customer Group', () => {
-		cy.visit(`app/customer/`);
-		cy.get('.primary-action').click();
-		cy.wait(500);
-		cy.get('.custom-actions > .btn').click();
-		cy.get_field('customer_group', 'Link').should('have.value', 'All Customer Groups');
-	});
-});
diff --git a/cypress/integration/test_item.js b/cypress/integration/test_item.js
deleted file mode 100644
index fcb7533..0000000
--- a/cypress/integration/test_item.js
+++ /dev/null
@@ -1,44 +0,0 @@
-describe("Test Item Dashboard", () => {
-	before(() => {
-		cy.login();
-		cy.visit("/app/item");
-		cy.insert_doc(
-			"Item",
-			{
-				item_code: "e2e_test_item",
-				item_group: "All Item Groups",
-				opening_stock: 42,
-				valuation_rate: 100,
-			},
-			true
-		);
-		cy.go_to_doc("item", "e2e_test_item");
-	});
-
-	it("should show dashboard with correct data on first load", () => {
-		cy.get(".stock-levels").contains("Stock Levels").should("be.visible");
-		cy.get(".stock-levels").contains("e2e_test_item").should("exist");
-
-		// reserved and available qty
-		cy.get(".stock-levels .inline-graph-count")
-			.eq(0)
-			.contains("0")
-			.should("exist");
-		cy.get(".stock-levels .inline-graph-count")
-			.eq(1)
-			.contains("42")
-			.should("exist");
-	});
-
-	it("should persist on field change", () => {
-		cy.get('input[data-fieldname="disabled"]').check();
-		cy.wait(500);
-		cy.get(".stock-levels").contains("Stock Levels").should("be.visible");
-		cy.get(".stock-levels").should("have.length", 1);
-	});
-
-	it("should persist on reload", () => {
-		cy.reload();
-		cy.get(".stock-levels").contains("Stock Levels").should("be.visible");
-	});
-});
diff --git a/cypress/integration/test_organizational_chart_desktop.js b/cypress/integration/test_organizational_chart_desktop.js
deleted file mode 100644
index 464cce4..0000000
--- a/cypress/integration/test_organizational_chart_desktop.js
+++ /dev/null
@@ -1,116 +0,0 @@
-context('Organizational Chart', () => {
-	before(() => {
-		cy.login();
-		cy.visit('/app/website');
-	});
-
-	it('navigates to org chart', () => {
-		cy.visit('/app');
-		cy.visit('/app/organizational-chart');
-		cy.url().should('include', '/organizational-chart');
-
-		cy.window().its('frappe.csrf_token').then(csrf_token => {
-			return cy.request({
-				url: `/api/method/erpnext.tests.ui_test_helpers.create_employee_records`,
-				method: 'POST',
-				headers: {
-					Accept: 'application/json',
-					'Content-Type': 'application/json',
-					'X-Frappe-CSRF-Token': csrf_token
-				},
-				timeout: 60000
-			}).then(res => {
-				expect(res.status).eq(200);
-				cy.get('.frappe-control[data-fieldname=company] input').focus().as('input');
-				cy.get('@input')
-					.clear({ force: true })
-					.type('Test Org Chart{downarrow}{enter}', { force: true })
-					.blur({ force: true });
-			});
-		});
-	});
-
-	it('renders root nodes and loads children for the first expandable node', () => {
-		// check rendered root nodes and the node name, title, connections
-		cy.get('.hierarchy').find('.root-level ul.node-children').children()
-			.should('have.length', 2)
-			.first()
-			.as('first-child');
-
-		cy.get('@first-child').get('.node-name').contains('Test Employee 1');
-		cy.get('@first-child').get('.node-info').find('.node-title').contains('CEO');
-		cy.get('@first-child').get('.node-info').find('.node-connections').contains('· 2 Connections');
-
-		cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
-			// children of 1st root visible
-			cy.get(`div[data-parent="${employee_records.message[0]}"]`).as('child-node');
-			cy.get('@child-node')
-				.should('have.length', 1)
-				.should('be.visible');
-			cy.get('@child-node').get('.node-name').contains('Test Employee 3');
-
-			// connectors between first root node and immediate child
-			cy.get(`path[data-parent="${employee_records.message[0]}"]`)
-				.should('be.visible')
-				.invoke('attr', 'data-child')
-				.should('equal', employee_records.message[2]);
-		});
-	});
-
-	it('hides active nodes children and connectors on expanding sibling node', () => {
-		cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
-			// click sibling
-			cy.get(`#${employee_records.message[1]}`)
-				.click()
-				.should('have.class', 'active');
-
-			// child nodes and connectors hidden
-			cy.get(`[data-parent="${employee_records.message[0]}"]`).should('not.be.visible');
-			cy.get(`path[data-parent="${employee_records.message[0]}"]`).should('not.be.visible');
-		});
-	});
-
-	it('collapses previous level nodes and refreshes connectors on expanding child node', () => {
-		cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
-			// click child node
-			cy.get(`#${employee_records.message[3]}`)
-				.click()
-				.should('have.class', 'active');
-
-			// previous level nodes: parent should be on active-path; other nodes should be collapsed
-			cy.get(`#${employee_records.message[0]}`).should('have.class', 'collapsed');
-			cy.get(`#${employee_records.message[1]}`).should('have.class', 'active-path');
-
-			// previous level connectors refreshed
-			cy.get(`path[data-parent="${employee_records.message[1]}"]`)
-				.should('have.class', 'collapsed-connector');
-
-			// child node's children and connectors rendered
-			cy.get(`[data-parent="${employee_records.message[3]}"]`).should('be.visible');
-			cy.get(`path[data-parent="${employee_records.message[3]}"]`).should('be.visible');
-		});
-	});
-
-	it('expands previous level nodes', () => {
-		cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
-			cy.get(`#${employee_records.message[0]}`)
-				.click()
-				.should('have.class', 'active');
-
-			cy.get(`[data-parent="${employee_records.message[0]}"]`)
-				.should('be.visible');
-
-			cy.get('ul.hierarchy').children().should('have.length', 2);
-			cy.get(`#connectors`).children().should('have.length', 1);
-		});
-	});
-
-	it('edit node navigates to employee master', () => {
-		cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
-			cy.get(`#${employee_records.message[0]}`).find('.btn-edit-node')
-				.click();
-
-			cy.url().should('include', `/employee/${employee_records.message[0]}`);
-		});
-	});
-});
diff --git a/cypress/integration/test_organizational_chart_mobile.js b/cypress/integration/test_organizational_chart_mobile.js
deleted file mode 100644
index 971ac6d..0000000
--- a/cypress/integration/test_organizational_chart_mobile.js
+++ /dev/null
@@ -1,195 +0,0 @@
-context('Organizational Chart Mobile', () => {
-	before(() => {
-		cy.login();
-		cy.visit('/app/website');
-	});
-
-	it('navigates to org chart', () => {
-		cy.viewport(375, 667);
-		cy.visit('/app');
-		cy.visit('/app/organizational-chart');
-		cy.url().should('include', '/organizational-chart');
-
-		cy.window().its('frappe.csrf_token').then(csrf_token => {
-			return cy.request({
-				url: `/api/method/erpnext.tests.ui_test_helpers.create_employee_records`,
-				method: 'POST',
-				headers: {
-					Accept: 'application/json',
-					'Content-Type': 'application/json',
-					'X-Frappe-CSRF-Token': csrf_token
-				},
-				timeout: 60000
-			}).then(res => {
-				expect(res.status).eq(200);
-				cy.get('.frappe-control[data-fieldname=company] input').focus().as('input');
-				cy.get('@input')
-					.clear({ force: true })
-					.type('Test Org Chart{downarrow}{enter}', { force: true })
-					.blur({ force: true });
-			});
-		});
-	});
-
-	it('renders root nodes', () => {
-		// check rendered root nodes and the node name, title, connections
-		cy.get('.hierarchy-mobile').find('.root-level').children()
-			.should('have.length', 2)
-			.first()
-			.as('first-child');
-
-		cy.get('@first-child').get('.node-name').contains('Test Employee 1');
-		cy.get('@first-child').get('.node-info').find('.node-title').contains('CEO');
-		cy.get('@first-child').get('.node-info').find('.node-connections').contains('· 2');
-	});
-
-	it('expands root node', () => {
-		cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
-			cy.get(`#${employee_records.message[1]}`)
-				.click()
-				.should('have.class', 'active');
-
-			// other root node removed
-			cy.get(`#${employee_records.message[0]}`).should('not.exist');
-
-			// children of active root node
-			cy.get('.hierarchy-mobile').find('.level').first().find('ul.node-children').children()
-				.should('have.length', 2);
-
-			cy.get(`div[data-parent="${employee_records.message[1]}"]`).first().as('child-node');
-			cy.get('@child-node').should('be.visible');
-
-			cy.get('@child-node')
-				.get('.node-name')
-				.contains('Test Employee 4');
-
-			// connectors between root node and immediate children
-			cy.get(`path[data-parent="${employee_records.message[1]}"]`).as('connectors');
-			cy.get('@connectors')
-				.should('have.length', 2)
-				.should('be.visible');
-
-			cy.get('@connectors')
-				.first()
-				.invoke('attr', 'data-child')
-				.should('eq', employee_records.message[3]);
-		});
-	});
-
-	it('expands child node', () => {
-		cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
-			cy.get(`#${employee_records.message[3]}`)
-				.click()
-				.should('have.class', 'active')
-				.as('expanded_node');
-
-			// 2 levels on screen; 1 on active path; 1 collapsed
-			cy.get('.hierarchy-mobile').children().should('have.length', 2);
-			cy.get(`#${employee_records.message[1]}`).should('have.class', 'active-path');
-
-			// children of expanded node visible
-			cy.get('@expanded_node')
-				.next()
-				.should('have.class', 'node-children')
-				.as('node-children');
-
-			cy.get('@node-children').children().should('have.length', 1);
-			cy.get('@node-children')
-				.first()
-				.get('.node-card')
-				.should('have.class', 'active-child')
-				.contains('Test Employee 7');
-
-			// orphan connectors removed
-			cy.get(`#connectors`).children().should('have.length', 2);
-		});
-	});
-
-	it('renders sibling group', () => {
-		cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
-			// sibling group visible for parent
-			cy.get(`#${employee_records.message[1]}`)
-				.next()
-				.as('sibling_group');
-
-			cy.get('@sibling_group')
-				.should('have.attr', 'data-parent', 'undefined')
-				.should('have.class', 'node-group')
-				.and('have.class', 'collapsed');
-
-			cy.get('@sibling_group').get('.avatar-group').children().as('siblings');
-			cy.get('@siblings').should('have.length', 1);
-			cy.get('@siblings')
-				.first()
-				.should('have.attr', 'title', 'Test Employee 1');
-
-		});
-	});
-
-	it('expands previous level nodes', () => {
-		cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
-			cy.get(`#${employee_records.message[6]}`)
-				.click()
-				.should('have.class', 'active');
-
-			// clicking on previous level node should remove all the nodes ahead
-			// and expand that node
-			cy.get(`#${employee_records.message[3]}`).click();
-			cy.get(`#${employee_records.message[3]}`)
-				.should('have.class', 'active')
-				.should('not.have.class', 'active-path');
-
-			cy.get(`#${employee_records.message[6]}`).should('have.class', 'active-child');
-			cy.get('.hierarchy-mobile').children().should('have.length', 2);
-			cy.get(`#connectors`).children().should('have.length', 2);
-		});
-	});
-
-	it('expands sibling group', () => {
-		cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
-			// sibling group visible for parent
-			cy.get(`#${employee_records.message[6]}`).click();
-
-			cy.get(`#${employee_records.message[3]}`)
-				.next()
-				.click();
-
-			// siblings of parent should be visible
-			cy.get('.hierarchy-mobile').prev().as('sibling_group');
-			cy.get('@sibling_group')
-				.should('exist')
-				.should('have.class', 'sibling-group')
-				.should('not.have.class', 'collapsed');
-
-			cy.get(`#${employee_records.message[1]}`)
-				.should('be.visible')
-				.should('have.class', 'active');
-
-			cy.get(`[data-parent="${employee_records.message[1]}"]`)
-				.should('be.visible')
-				.should('have.length', 2)
-				.should('have.class', 'active-child');
-		});
-	});
-
-	it('goes to the respective level after clicking on non-collapsed sibling group', () => {
-		cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(() => {
-			// click on non-collapsed sibling group
-			cy.get('.hierarchy-mobile')
-				.prev()
-				.click();
-
-			// should take you to that level
-			cy.get('.hierarchy-mobile').find('li.level .node-card').should('have.length', 2);
-		});
-	});
-
-	it('edit node navigates to employee master', () => {
-		cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
-			cy.get(`#${employee_records.message[0]}`).find('.btn-edit-node')
-				.click();
-
-			cy.url().should('include', `/employee/${employee_records.message[0]}`);
-		});
-	});
-});
diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js
deleted file mode 100644
index 07d9804..0000000
--- a/cypress/plugins/index.js
+++ /dev/null
@@ -1,17 +0,0 @@
-// ***********************************************************
-// This example plugins/index.js can be used to load plugins
-//
-// You can change the location of this file or turn off loading
-// the plugins file with the 'pluginsFile' configuration option.
-//
-// You can read more here:
-// https://on.cypress.io/plugins-guide
-// ***********************************************************
-
-// This function is called when a project is opened or re-opened (e.g. due to
-// the project's config changing)
-
-module.exports = () => {
-	// `on` is used to hook into various events Cypress emits
-	// `config` is the resolved Cypress config
-};
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
deleted file mode 100644
index 7ddc80a..0000000
--- a/cypress/support/commands.js
+++ /dev/null
@@ -1,31 +0,0 @@
-// ***********************************************
-// This example commands.js shows you how to
-// create various custom commands and overwrite
-// existing commands.
-//
-// For more comprehensive examples of custom
-// commands please read more here:
-// https://on.cypress.io/custom-commands
-// ***********************************************
-//
-//
-// -- This is a parent command --
-// Cypress.Commands.add("login", (email, password) => { ... });
-//
-//
-// -- This is a child command --
-// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... });
-//
-//
-// -- This is a dual command --
-// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... });
-//
-//
-// -- This is will overwrite an existing command --
-// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... });
-
-const slug = (name) => name.toLowerCase().replace(" ", "-");
-
-Cypress.Commands.add("go_to_doc", (doctype, name) => {
-	cy.visit(`/app/${slug(doctype)}/${encodeURIComponent(name)}`);
-});
diff --git a/cypress/support/index.js b/cypress/support/index.js
deleted file mode 100644
index 72070cc..0000000
--- a/cypress/support/index.js
+++ /dev/null
@@ -1,26 +0,0 @@
-// ***********************************************************
-// This example support/index.js is processed and
-// loaded automatically before your test files.
-//
-// This is a great place to put global configuration and
-// behavior that modifies Cypress.
-//
-// You can change the location of this file or turn off
-// automatically serving support files with the
-// 'supportFile' configuration option.
-//
-// You can read more here:
-// https://on.cypress.io/configuration
-// ***********************************************************
-
-// Import commands.js using ES2015 syntax:
-import './commands';
-import '../../../frappe/cypress/support/commands' // eslint-disable-line
-
-
-// Alternatively you can use CommonJS syntax:
-// require('./commands')
-
-Cypress.Cookies.defaults({
-	preserve: 'sid'
-});
diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json
deleted file mode 100644
index d90ebf6..0000000
--- a/cypress/tsconfig.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
-    "compilerOptions": {
-        "allowJs": true,
-        "baseUrl": "../node_modules",
-        "types": [
-            "cypress"
-        ]
-    },
-    "include": [
-        "**/*.*"
-    ]
-}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py
index 53b1c64..5a86376 100644
--- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py
+++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py
@@ -54,8 +54,8 @@
 
 		pce = frappe.db.sql(
 			"""select name from `tabPeriod Closing Voucher`
-			where posting_date > %s and fiscal_year = %s and docstatus = 1""",
-			(self.posting_date, self.fiscal_year),
+			where posting_date > %s and fiscal_year = %s and docstatus = 1 and company = %s""",
+			(self.posting_date, self.fiscal_year, self.company),
 		)
 		if pce and pce[0][0]:
 			frappe.throw(
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index 42917f8..7e3597e 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -45,8 +45,6 @@
 		if (this.frm.doc.supplier && this.frm.doc.__islocal) {
 			this.frm.trigger('supplier');
 		}
-
-		erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
 	}
 
 	refresh(doc) {
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index e6da666..23ad223 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -545,7 +545,16 @@
 					from_repost=from_repost,
 				)
 			elif self.docstatus == 2:
+				provisional_entries = [a for a in gl_entries if a.voucher_type == "Purchase Receipt"]
 				make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
+				if provisional_entries:
+					for entry in provisional_entries:
+						frappe.db.set_value(
+							"GL Entry",
+							{"voucher_type": "Purchase Receipt", "voucher_detail_no": entry.voucher_detail_no},
+							"is_cancelled",
+							1,
+						)
 
 			if update_outstanding == "No":
 				update_outstanding_amt(
@@ -1127,7 +1136,7 @@
 		# Stock ledger value is not matching with the warehouse amount
 		if (
 			self.update_stock
-			and voucher_wise_stock_value.get(item.name)
+			and voucher_wise_stock_value.get((item.name, item.warehouse))
 			and warehouse_debit_amount
 			!= flt(voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision)
 		):
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index 30d26ac..3c70e24 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -27,12 +27,13 @@
 	make_purchase_receipt,
 )
 from erpnext.stock.doctype.stock_entry.test_stock_entry import get_qty_after_transaction
+from erpnext.stock.tests.test_utils import StockTestMixin
 
 test_dependencies = ["Item", "Cost Center", "Payment Term", "Payment Terms Template"]
 test_ignore = ["Serial No"]
 
 
-class TestPurchaseInvoice(unittest.TestCase):
+class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
 	@classmethod
 	def setUpClass(self):
 		unlink_payment_on_cancel_of_invoice()
@@ -693,6 +694,80 @@
 			self.assertEqual(expected_values[gle.account][0], gle.debit)
 			self.assertEqual(expected_values[gle.account][1], gle.credit)
 
+	def test_standalone_return_using_pi(self):
+		from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
+
+		item = self.make_item().name
+		company = "_Test Company with perpetual inventory"
+		warehouse = "Stores - TCP1"
+
+		make_stock_entry(item_code=item, target=warehouse, qty=50, rate=120)
+
+		return_pi = make_purchase_invoice(
+			is_return=1,
+			item=item,
+			qty=-10,
+			update_stock=1,
+			rate=100,
+			company=company,
+			warehouse=warehouse,
+			cost_center="Main - TCP1",
+		)
+
+		# assert that stock consumption is with actual rate
+		self.assertGLEs(
+			return_pi,
+			[{"credit": 1200, "debit": 0}],
+			gle_filters={"account": "Stock In Hand - TCP1"},
+		)
+
+		# assert loss booked in COGS
+		self.assertGLEs(
+			return_pi,
+			[{"credit": 0, "debit": 200}],
+			gle_filters={"account": "Cost of Goods Sold - TCP1"},
+		)
+
+	def test_return_with_lcv(self):
+		from erpnext.controllers.sales_and_purchase_return import make_return_doc
+		from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import (
+			create_landed_cost_voucher,
+		)
+
+		item = self.make_item().name
+		company = "_Test Company with perpetual inventory"
+		warehouse = "Stores - TCP1"
+		cost_center = "Main - TCP1"
+
+		pi = make_purchase_invoice(
+			item=item,
+			company=company,
+			warehouse=warehouse,
+			cost_center=cost_center,
+			update_stock=1,
+			qty=10,
+			rate=100,
+		)
+
+		# Create landed cost voucher - will increase valuation of received item by 10
+		create_landed_cost_voucher("Purchase Invoice", pi.name, pi.company, charges=100)
+		return_pi = make_return_doc(pi.doctype, pi.name)
+		return_pi.save().submit()
+
+		# assert that stock consumption is with actual in rate
+		self.assertGLEs(
+			return_pi,
+			[{"credit": 1100, "debit": 0}],
+			gle_filters={"account": "Stock In Hand - TCP1"},
+		)
+
+		# assert loss booked in COGS
+		self.assertGLEs(
+			return_pi,
+			[{"credit": 0, "debit": 100}],
+			gle_filters={"account": "Cost of Goods Sold - TCP1"},
+		)
+
 	def test_multi_currency_gle(self):
 		pi = make_purchase_invoice(
 			supplier="_Test Supplier USD",
@@ -1526,6 +1601,18 @@
 
 		check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date)
 
+		# Cancel purchase invoice to check reverse provisional entry cancellation
+		pi.cancel()
+
+		expected_gle_for_purchase_receipt_post_pi_cancel = [
+			["Provision Account - _TC", 0, 250, pi.posting_date],
+			["_Test Account Cost for Goods Sold - _TC", 250, 0, pi.posting_date],
+		]
+
+		check_gl_entries(
+			self, pr.name, expected_gle_for_purchase_receipt_post_pi_cancel, pr.posting_date
+		)
+
 		company.enable_provisional_accounting_for_non_stock_items = 0
 		company.save()
 
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index 9dde85f..aefa9a5 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -52,7 +52,6 @@
 			me.frm.refresh_fields();
 		}
 		erpnext.queries.setup_warehouse_query(this.frm);
-		erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
 	}
 
 	refresh(doc, dt, dn) {
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index 80b95db..327545a 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -1790,6 +1790,8 @@
    "width": "50%"
   },
   {
+   "fetch_from": "sales_partner.commission_rate",
+   "fetch_if_empty": 1,
    "fieldname": "commission_rate",
    "fieldtype": "Float",
    "hide_days": 1,
@@ -2038,7 +2040,7 @@
    "link_fieldname": "consolidated_invoice"
   }
  ],
- "modified": "2022-03-08 16:08:53.517903",
+ "modified": "2022-06-10 03:52:51.409913",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Sales Invoice",
diff --git a/erpnext/accounts/module_onboarding/accounts/accounts.json b/erpnext/accounts/module_onboarding/accounts/accounts.json
index aa7cdf7..b9040e3 100644
--- a/erpnext/accounts/module_onboarding/accounts/accounts.json
+++ b/erpnext/accounts/module_onboarding/accounts/accounts.json
@@ -13,7 +13,7 @@
  "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/accounts",
  "idx": 0,
  "is_complete": 0,
- "modified": "2022-01-18 18:35:52.326688",
+ "modified": "2022-06-07 14:29:21.352132",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Accounts",
diff --git a/erpnext/accounts/onboarding_step/chart_of_accounts/chart_of_accounts.json b/erpnext/accounts/onboarding_step/chart_of_accounts/chart_of_accounts.json
index 67553ba..0973ab3 100644
--- a/erpnext/accounts/onboarding_step/chart_of_accounts/chart_of_accounts.json
+++ b/erpnext/accounts/onboarding_step/chart_of_accounts/chart_of_accounts.json
@@ -1,8 +1,8 @@
 {
- "action": "Watch Video",
+ "action": "Go to Page",
  "action_label": "Learn more about Chart of Accounts",
  "callback_message": "You can continue with the onboarding after exploring this page",
- "callback_title": "Awesome Work",
+ "callback_title": "Explore Chart of Accounts",
  "creation": "2020-05-13 19:58:20.928127",
  "description": "# Chart Of Accounts\n\nERPNext sets up a simple chart of accounts for each Company you create, but you can modify it according to business and legal requirements.",
  "docstatus": 0,
@@ -12,7 +12,7 @@
  "is_complete": 0,
  "is_single": 0,
  "is_skipped": 0,
- "modified": "2021-08-13 11:46:25.878506",
+ "modified": "2022-06-07 14:21:26.264769",
  "modified_by": "Administrator",
  "name": "Chart of Accounts",
  "owner": "Administrator",
diff --git a/erpnext/accounts/onboarding_step/setup_taxes/setup_taxes.json b/erpnext/accounts/onboarding_step/setup_taxes/setup_taxes.json
index 9f4c873..b6e9f5c 100644
--- a/erpnext/accounts/onboarding_step/setup_taxes/setup_taxes.json
+++ b/erpnext/accounts/onboarding_step/setup_taxes/setup_taxes.json
@@ -2,14 +2,14 @@
  "action": "Create Entry",
  "action_label": "Manage Sales Tax Templates",
  "creation": "2020-05-13 19:29:43.844463",
- "description": "# Setting up Taxes\n\nERPNext lets you configure your taxes so that they are automatically applied in your buying and selling transactions. You can configure them globally or even on Items. ERPNext taxes are pre-configured for most regions.\n",
+ "description": "# Setting up Taxes\n\nERPNext lets you configure your taxes so that they are automatically applied in your buying and selling transactions. You can configure them globally or even on Items. ERPNext taxes are pre-configured for most regions.\n\n[Checkout pre-configured taxes](/app/sales-taxes-and-charges-template)\n",
  "docstatus": 0,
  "doctype": "Onboarding Step",
  "idx": 0,
  "is_complete": 0,
  "is_single": 0,
  "is_skipped": 0,
- "modified": "2021-08-13 11:48:37.238610",
+ "modified": "2022-06-07 14:27:15.906286",
  "modified_by": "Administrator",
  "name": "Setup Taxes",
  "owner": "Administrator",
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.html b/erpnext/accounts/report/accounts_receivable/accounts_receivable.html
index f4fd06b..f2bf942 100644
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.html
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.html
@@ -42,7 +42,7 @@
 
 	{% if(filters.show_future_payments) { %}
 		{% var balance_row = data.slice(-1).pop();
-			var start = filters.based_on_payment_terms ? 13 : 11;
+			var start = report.columns.findIndex((elem) => (elem.fieldname == 'age'));
 			var range1 = report.columns[start].label;
 			var range2 = report.columns[start+1].label;
 			var range3 = report.columns[start+2].label;
diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js
index d3e836a..dd965a9 100644
--- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js
+++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js
@@ -50,7 +50,15 @@
 				"fieldtype": "Link",
 				"options": "Fiscal Year",
 				"default": frappe.defaults.get_user_default("fiscal_year"),
-				"reqd": 1
+				"reqd": 1,
+				on_change: () => {
+					frappe.model.with_doc("Fiscal Year", frappe.query_report.get_filter_value('from_fiscal_year'), function(r) {
+						let year_start_date = frappe.model.get_value("Fiscal Year", frappe.query_report.get_filter_value('from_fiscal_year'), "year_start_date");
+						frappe.query_report.set_filter_value({
+							period_start_date: year_start_date
+						});
+					});
+				}
 			},
 			{
 				"fieldname":"to_fiscal_year",
@@ -58,7 +66,15 @@
 				"fieldtype": "Link",
 				"options": "Fiscal Year",
 				"default": frappe.defaults.get_user_default("fiscal_year"),
-				"reqd": 1
+				"reqd": 1,
+				on_change: () => {
+					frappe.model.with_doc("Fiscal Year", frappe.query_report.get_filter_value('to_fiscal_year'), function(r) {
+						let year_end_date = frappe.model.get_value("Fiscal Year", frappe.query_report.get_filter_value('to_fiscal_year'), "year_end_date");
+						frappe.query_report.set_filter_value({
+							period_end_date: year_end_date
+						});
+					});
+				}
 			},
 			{
 				"fieldname":"finance_book",
diff --git a/erpnext/accounts/report/gross_profit/gross_profit.js b/erpnext/accounts/report/gross_profit/gross_profit.js
index 158ff4d..3d37b58 100644
--- a/erpnext/accounts/report/gross_profit/gross_profit.js
+++ b/erpnext/accounts/report/gross_profit/gross_profit.js
@@ -35,7 +35,7 @@
 			"fieldname":"group_by",
 			"label": __("Group By"),
 			"fieldtype": "Select",
-			"options": "Invoice\nItem Code\nItem Group\nBrand\nWarehouse\nCustomer\nCustomer Group\nTerritory\nSales Person\nProject",
+			"options": "Invoice\nItem Code\nItem Group\nBrand\nWarehouse\nCustomer\nCustomer Group\nTerritory\nSales Person\nProject\nMonthly\nPayment Term",
 			"default": "Invoice"
 		},
 	],
diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py
index 9668992..526ea9d 100644
--- a/erpnext/accounts/report/gross_profit/gross_profit.py
+++ b/erpnext/accounts/report/gross_profit/gross_profit.py
@@ -4,7 +4,7 @@
 
 import frappe
 from frappe import _, scrub
-from frappe.utils import cint, flt
+from frappe.utils import cint, flt, formatdate
 
 from erpnext.controllers.queries import get_match_cond
 from erpnext.stock.utils import get_incoming_rate
@@ -124,6 +124,23 @@
 				"gross_profit",
 				"gross_profit_percent",
 			],
+			"monthly": [
+				"monthly",
+				"qty",
+				"base_rate",
+				"buying_rate",
+				"base_amount",
+				"buying_amount",
+				"gross_profit",
+				"gross_profit_percent",
+			],
+			"payment_term": [
+				"payment_term",
+				"base_amount",
+				"buying_amount",
+				"gross_profit",
+				"gross_profit_percent",
+			],
 		}
 	)
 
@@ -317,6 +334,19 @@
 				"options": "territory",
 				"width": 100,
 			},
+			"monthly": {
+				"label": _("Monthly"),
+				"fieldname": "monthly",
+				"fieldtype": "Data",
+				"width": 100,
+			},
+			"payment_term": {
+				"label": _("Payment Term"),
+				"fieldname": "payment_term",
+				"fieldtype": "Link",
+				"options": "Payment Term",
+				"width": 170,
+			},
 		}
 	)
 
@@ -390,6 +420,9 @@
 			buying_amount = 0
 
 		for row in reversed(self.si_list):
+			if self.filters.get("group_by") == "Monthly":
+				row.monthly = formatdate(row.posting_date, "MMM YYYY")
+
 			if self.skip_row(row):
 				continue
 
@@ -445,17 +478,7 @@
 
 	def get_average_rate_based_on_group_by(self):
 		for key in list(self.grouped):
-			if self.filters.get("group_by") != "Invoice":
-				for i, row in enumerate(self.grouped[key]):
-					if i == 0:
-						new_row = row
-					else:
-						new_row.qty += flt(row.qty)
-						new_row.buying_amount += flt(row.buying_amount, self.currency_precision)
-						new_row.base_amount += flt(row.base_amount, self.currency_precision)
-				new_row = self.set_average_rate(new_row)
-				self.grouped_data.append(new_row)
-			else:
+			if self.filters.get("group_by") == "Invoice":
 				for i, row in enumerate(self.grouped[key]):
 					if row.indent == 1.0:
 						if (
@@ -469,6 +492,44 @@
 						if flt(row.qty) or row.base_amount:
 							row = self.set_average_rate(row)
 							self.grouped_data.append(row)
+			elif self.filters.get("group_by") == "Payment Term":
+				for i, row in enumerate(self.grouped[key]):
+					invoice_portion = 0
+
+					if row.is_return:
+						invoice_portion = 100
+					elif row.invoice_portion:
+						invoice_portion = row.invoice_portion
+					else:
+						invoice_portion = row.payment_amount * 100 / row.base_net_amount
+
+					if i == 0:
+						new_row = row
+						self.set_average_based_on_payment_term_portion(new_row, row, invoice_portion)
+					else:
+						new_row.qty += flt(row.qty)
+						self.set_average_based_on_payment_term_portion(new_row, row, invoice_portion, True)
+
+				new_row = self.set_average_rate(new_row)
+				self.grouped_data.append(new_row)
+			else:
+				for i, row in enumerate(self.grouped[key]):
+					if i == 0:
+						new_row = row
+					else:
+						new_row.qty += flt(row.qty)
+						new_row.buying_amount += flt(row.buying_amount, self.currency_precision)
+						new_row.base_amount += flt(row.base_amount, self.currency_precision)
+				new_row = self.set_average_rate(new_row)
+				self.grouped_data.append(new_row)
+
+	def set_average_based_on_payment_term_portion(self, new_row, row, invoice_portion, aggr=False):
+		cols = ["base_amount", "buying_amount", "gross_profit"]
+		for col in cols:
+			if aggr:
+				new_row[col] += row[col] * invoice_portion / 100
+			else:
+				new_row[col] = row[col] * invoice_portion / 100
 
 	def is_not_invoice_row(self, row):
 		return (self.filters.get("group_by") == "Invoice" and row.indent != 0.0) or self.filters.get(
@@ -622,6 +683,20 @@
 			sales_person_cols = ""
 			sales_team_table = ""
 
+		if self.filters.group_by == "Payment Term":
+			payment_term_cols = """,if(`tabSales Invoice`.is_return = 1,
+										'{0}',
+										coalesce(schedule.payment_term, '{1}')) as payment_term,
+									schedule.invoice_portion,
+									schedule.payment_amount """.format(
+				_("Sales Return"), _("No Terms")
+			)
+			payment_term_table = """ left join `tabPayment Schedule` schedule on schedule.parent = `tabSales Invoice`.name and
+																				`tabSales Invoice`.is_return = 0 """
+		else:
+			payment_term_cols = ""
+			payment_term_table = ""
+
 		if self.filters.get("sales_invoice"):
 			conditions += " and `tabSales Invoice`.name = %(sales_invoice)s"
 
@@ -644,10 +719,12 @@
 				`tabSales Invoice Item`.name as "item_row", `tabSales Invoice`.is_return,
 				`tabSales Invoice Item`.cost_center
 				{sales_person_cols}
+				{payment_term_cols}
 			from
 				`tabSales Invoice` inner join `tabSales Invoice Item`
 					on `tabSales Invoice Item`.parent = `tabSales Invoice`.name
 				{sales_team_table}
+				{payment_term_table}
 			where
 				`tabSales Invoice`.docstatus=1 and `tabSales Invoice`.is_opening!='Yes' {conditions} {match_cond}
 			order by
@@ -655,6 +732,8 @@
 				conditions=conditions,
 				sales_person_cols=sales_person_cols,
 				sales_team_table=sales_team_table,
+				payment_term_cols=payment_term_cols,
+				payment_term_table=payment_term_table,
 				match_cond=get_match_cond("Sales Invoice"),
 			),
 			self.filters,
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 2e7213f..ac70666 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
@@ -443,12 +443,6 @@
 	]  # nosec
 
 
-def get_deducted_taxes():
-	return frappe.db.sql_list(
-		"select name from `tabPurchase Taxes and Charges` where add_deduct_tax = 'Deduct'"
-	)
-
-
 def get_tax_accounts(
 	item_list,
 	columns,
@@ -462,6 +456,7 @@
 	tax_columns = []
 	invoice_item_row = {}
 	itemised_tax = {}
+	add_deduct_tax = "charge_type"
 
 	tax_amount_precision = (
 		get_field_precision(
@@ -477,13 +472,13 @@
 	conditions = ""
 	if doctype == "Purchase Invoice":
 		conditions = " and category in ('Total', 'Valuation and Total') and base_tax_amount_after_discount_amount != 0"
+		add_deduct_tax = "add_deduct_tax"
 
-	deducted_tax = get_deducted_taxes()
 	tax_details = frappe.db.sql(
 		"""
 		select
 			name, parent, description, item_wise_tax_detail,
-			charge_type, base_tax_amount_after_discount_amount
+			charge_type, {add_deduct_tax}, base_tax_amount_after_discount_amount
 		from `tab%s`
 		where
 			parenttype = %s and docstatus = 1
@@ -491,12 +486,22 @@
 			and parent in (%s)
 			%s
 		order by description
-	"""
+	""".format(
+			add_deduct_tax=add_deduct_tax
+		)
 		% (tax_doctype, "%s", ", ".join(["%s"] * len(invoice_item_row)), conditions),
 		tuple([doctype] + list(invoice_item_row)),
 	)
 
-	for name, parent, description, item_wise_tax_detail, charge_type, tax_amount in tax_details:
+	for (
+		name,
+		parent,
+		description,
+		item_wise_tax_detail,
+		charge_type,
+		add_deduct_tax,
+		tax_amount,
+	) in tax_details:
 		description = handle_html(description)
 		if description not in tax_columns and tax_amount:
 			# as description is text editor earlier and markup can break the column convention in reports
@@ -529,7 +534,9 @@
 						if item_tax_amount:
 							tax_value = flt(item_tax_amount, tax_amount_precision)
 							tax_value = (
-								tax_value * -1 if (doctype == "Purchase Invoice" and name in deducted_tax) else tax_value
+								tax_value * -1
+								if (doctype == "Purchase Invoice" and add_deduct_tax == "Deduct")
+								else tax_value
 							)
 
 							itemised_tax.setdefault(d.name, {})[description] = frappe._dict(
diff --git a/erpnext/accounts/report/profitability_analysis/profitability_analysis.py b/erpnext/accounts/report/profitability_analysis/profitability_analysis.py
index 3e7aa1e..183e279 100644
--- a/erpnext/accounts/report/profitability_analysis/profitability_analysis.py
+++ b/erpnext/accounts/report/profitability_analysis/profitability_analysis.py
@@ -211,6 +211,7 @@
 		{additional_conditions}
 		and posting_date <= %(to_date)s
 		and {based_on} is not null
+		and is_cancelled = 0
 		order by {based_on}, posting_date""".format(
 			additional_conditions="\n".join(additional_conditions), based_on=based_on
 		),
diff --git a/erpnext/accounts/report/sales_register/sales_register.py b/erpnext/accounts/report/sales_register/sales_register.py
index 34b3f03..33bd3c7 100644
--- a/erpnext/accounts/report/sales_register/sales_register.py
+++ b/erpnext/accounts/report/sales_register/sales_register.py
@@ -346,9 +346,13 @@
 def get_conditions(filters):
 	conditions = ""
 
+	accounting_dimensions = get_accounting_dimensions(as_list=False) or []
+	accounting_dimensions_list = [d.fieldname for d in accounting_dimensions]
+
 	if filters.get("company"):
 		conditions += " and company=%(company)s"
-	if filters.get("customer"):
+
+	if filters.get("customer") and "customer" not in accounting_dimensions_list:
 		conditions += " and customer = %(customer)s"
 
 	if filters.get("from_date"):
@@ -359,32 +363,18 @@
 	if filters.get("owner"):
 		conditions += " and owner = %(owner)s"
 
-	if filters.get("mode_of_payment"):
-		conditions += """ and exists(select name from `tabSales Invoice Payment`
-			 where parent=`tabSales Invoice`.name
-			 	and ifnull(`tabSales Invoice Payment`.mode_of_payment, '') = %(mode_of_payment)s)"""
+	def get_sales_invoice_item_field_condition(field, table="Sales Invoice Item") -> str:
+		if not filters.get(field) or field in accounting_dimensions_list:
+			return ""
+		return f""" and exists(select name from `tab{table}`
+				where parent=`tabSales Invoice`.name
+					and ifnull(`tab{table}`.{field}, '') = %({field})s)"""
 
-	if filters.get("cost_center"):
-		conditions += """ and exists(select name from `tabSales Invoice Item`
-			 where parent=`tabSales Invoice`.name
-			 	and ifnull(`tabSales Invoice Item`.cost_center, '') = %(cost_center)s)"""
-
-	if filters.get("warehouse"):
-		conditions += """ and exists(select name from `tabSales Invoice Item`
-			 where parent=`tabSales Invoice`.name
-			 	and ifnull(`tabSales Invoice Item`.warehouse, '') = %(warehouse)s)"""
-
-	if filters.get("brand"):
-		conditions += """ and exists(select name from `tabSales Invoice Item`
-			 where parent=`tabSales Invoice`.name
-			 	and ifnull(`tabSales Invoice Item`.brand, '') = %(brand)s)"""
-
-	if filters.get("item_group"):
-		conditions += """ and exists(select name from `tabSales Invoice Item`
-			 where parent=`tabSales Invoice`.name
-			 	and ifnull(`tabSales Invoice Item`.item_group, '') = %(item_group)s)"""
-
-	accounting_dimensions = get_accounting_dimensions(as_list=False)
+	conditions += get_sales_invoice_item_field_condition("mode_of_payments", "Sales Invoice Payment")
+	conditions += get_sales_invoice_item_field_condition("cost_center")
+	conditions += get_sales_invoice_item_field_condition("warehouse")
+	conditions += get_sales_invoice_item_field_condition("brand")
+	conditions += get_sales_invoice_item_field_condition("item_group")
 
 	if accounting_dimensions:
 		common_condition = """
diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py
index e5a4ed2..6bd08ad 100644
--- a/erpnext/accounts/report/trial_balance/trial_balance.py
+++ b/erpnext/accounts/report/trial_balance/trial_balance.py
@@ -160,14 +160,12 @@
 	if filters.project:
 		additional_conditions += " and project = %(project)s"
 
-	if filters.finance_book:
-		fb_conditions = " AND finance_book = %(finance_book)s"
-		if filters.include_default_book_entries:
-			fb_conditions = (
-				" AND (finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)"
-			)
-
-		additional_conditions += fb_conditions
+	if filters.get("include_default_book_entries"):
+		additional_conditions += (
+			" AND (finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)"
+		)
+	else:
+		additional_conditions += " AND (finance_book in (%(finance_book)s, '') OR finance_book IS NULL)"
 
 	accounting_dimensions = get_accounting_dimensions(as_list=False)
 
diff --git a/erpnext/accounts/test/test_reports.py b/erpnext/accounts/test/test_reports.py
index 19fe74f..3f06c30 100644
--- a/erpnext/accounts/test/test_reports.py
+++ b/erpnext/accounts/test/test_reports.py
@@ -28,6 +28,7 @@
 	("Item-wise Sales Register", {}),
 	("Item-wise Purchase Register", {}),
 	("Sales Register", {}),
+	("Sales Register", {"item_group": "All Item Groups"}),
 	("Purchase Register", {}),
 	(
 		"Tax Detail",
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 1869cc7..8711395 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -1124,6 +1124,9 @@
 def repost_gle_for_stock_vouchers(
 	stock_vouchers, posting_date, company=None, warehouse_account=None
 ):
+
+	from erpnext.accounts.general_ledger import toggle_debit_credit_if_negative
+
 	if not stock_vouchers:
 		return
 
@@ -1142,10 +1145,12 @@
 	precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit")) or 2
 
 	gle = get_voucherwise_gl_entries(stock_vouchers, posting_date)
-	for voucher_type, voucher_no in stock_vouchers:
+	for idx, (voucher_type, voucher_no) in enumerate(stock_vouchers):
 		existing_gle = gle.get((voucher_type, voucher_no), [])
-		voucher_obj = frappe.get_cached_doc(voucher_type, voucher_no)
-		expected_gle = voucher_obj.get_gl_entries(warehouse_account)
+		voucher_obj = frappe.get_doc(voucher_type, voucher_no)
+		# Some transactions post credit as negative debit, this is handled while posting GLE
+		# but while comparing we need to make sure it's flipped so comparisons are accurate
+		expected_gle = toggle_debit_credit_if_negative(voucher_obj.get_gl_entries(warehouse_account))
 		if expected_gle:
 			if not existing_gle or not compare_existing_and_expected_gle(
 				existing_gle, expected_gle, precision
@@ -1155,6 +1160,11 @@
 		else:
 			_delete_gl_entries(voucher_type, voucher_no)
 
+		if idx % 20 == 0:
+			# Commit every 20 documents to avoid losing progress
+			# and reducing memory usage
+			frappe.db.commit()
+
 
 def sort_stock_vouchers_by_posting_date(
 	stock_vouchers: List[Tuple[str, str]]
diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json
index 89a9448..6c18a46 100644
--- a/erpnext/buying/doctype/buying_settings/buying_settings.json
+++ b/erpnext/buying/doctype/buying_settings/buying_settings.json
@@ -148,7 +148,7 @@
  "index_web_pages_for_search": 1,
  "issingle": 1,
  "links": [],
- "modified": "2022-04-14 15:56:42.340223",
+ "modified": "2022-05-31 19:40:26.103909",
  "modified_by": "Administrator",
  "module": "Buying",
  "name": "Buying Settings",
@@ -162,6 +162,16 @@
    "role": "System Manager",
    "share": 1,
    "write": 1
+  },
+  {
+   "create": 1,
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "role": "Purchase Manager",
+   "share": 1,
+   "write": 1
   }
  ],
  "sort_field": "modified",
diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.py b/erpnext/buying/doctype/buying_settings/buying_settings.py
index c52b59e..7b18cdb 100644
--- a/erpnext/buying/doctype/buying_settings/buying_settings.py
+++ b/erpnext/buying/doctype/buying_settings/buying_settings.py
@@ -18,7 +18,7 @@
 		for key in ["supplier_group", "supp_master_name", "maintain_same_rate", "buying_price_list"]:
 			frappe.db.set_default(key, self.get(key, ""))
 
-		from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series
+		from erpnext.utilities.naming import set_by_naming_series
 
 		set_by_naming_series(
 			"Supplier",
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js
index c9e6798..da45610 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.js
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.js
@@ -43,8 +43,6 @@
 		erpnext.queries.setup_queries(frm, "Warehouse", function() {
 			return erpnext.queries.warehouse(frm.doc);
 		});
-
-		erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
 	},
 
 	apply_tds: function(frm) {
diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py
index 97d0ba0..43152e8 100644
--- a/erpnext/buying/doctype/supplier/supplier.py
+++ b/erpnext/buying/doctype/supplier/supplier.py
@@ -84,6 +84,9 @@
 		self.save()
 
 	def validate_internal_supplier(self):
+		if not self.is_internal_supplier:
+			self.represents_company = ""
+
 		internal_supplier = frappe.db.get_value(
 			"Supplier",
 			{
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 056084b..854c0d0 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -1465,8 +1465,8 @@
 
 		if not party_gle_currency and (party_account_currency != self.currency):
 			frappe.throw(
-				_("Party Account {0} currency and document currency should be same").format(
-					frappe.bold(party_account)
+				_("Party Account {0} currency ({1}) and document currency ({2}) should be same").format(
+					frappe.bold(party_account), party_account_currency, self.currency
 				)
 			)
 
@@ -1866,7 +1866,7 @@
 def get_taxes_and_charges(master_doctype, master_name):
 	if not master_name:
 		return
-	from frappe.model import default_fields
+	from frappe.model import child_table_fields, default_fields
 
 	tax_master = frappe.get_doc(master_doctype, master_name)
 
@@ -1874,7 +1874,7 @@
 	for i, tax in enumerate(tax_master.get("taxes")):
 		tax = tax.as_dict()
 
-		for fieldname in default_fields:
+		for fieldname in default_fields + child_table_fields:
 			if fieldname in tax:
 				del tax[fieldname]
 
@@ -2661,7 +2661,8 @@
 			parent.update_reserved_qty_for_subcontract()
 			parent.create_raw_materials_supplied("supplied_items")
 			parent.save()
-	else:
+	else:  # Sales Order
+		parent.validate_warehouse()
 		parent.update_reserved_qty()
 		parent.update_project()
 		parent.update_prevdoc_status("submit")
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index bd4b59b..d24ac3f 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -316,7 +316,7 @@
 	return data[0]
 
 
-def make_return_doc(doctype, source_name, target_doc=None):
+def make_return_doc(doctype: str, source_name: str, target_doc=None):
 	from frappe.model.mapper import get_mapped_doc
 
 	from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
diff --git a/erpnext/e_commerce/doctype/website_item/website_item.py b/erpnext/e_commerce/doctype/website_item/website_item.py
index 02ec3bf..f6fea72 100644
--- a/erpnext/e_commerce/doctype/website_item/website_item.py
+++ b/erpnext/e_commerce/doctype/website_item/website_item.py
@@ -34,9 +34,7 @@
 
 	def autoname(self):
 		# use naming series to accomodate items with same name (different item code)
-		from frappe.model.naming import make_autoname
-
-		from erpnext.setup.doctype.naming_series.naming_series import get_default_naming_series
+		from frappe.model.naming import get_default_naming_series, make_autoname
 
 		naming_series = get_default_naming_series("Website Item")
 		if not self.name and naming_series:
diff --git a/erpnext/e_commerce/redisearch_utils.py b/erpnext/e_commerce/redisearch_utils.py
index 61b4b9e..1f649c7 100644
--- a/erpnext/e_commerce/redisearch_utils.py
+++ b/erpnext/e_commerce/redisearch_utils.py
@@ -38,7 +38,7 @@
 		out = cache.execute_command("MODULE LIST")
 
 		parsed_output = " ".join(
-			(" ".join([s.decode() for s in o if not isinstance(s, int)]) for o in out)
+			(" ".join([frappe.as_unicode(s) for s in o if not isinstance(s, int)]) for o in out)
 		)
 		return "search" in parsed_output
 	except Exception:
diff --git a/erpnext/erpnext_integrations/connectors/github_connection.py b/erpnext/erpnext_integrations/connectors/github_connection.py
deleted file mode 100644
index f28065e..0000000
--- a/erpnext/erpnext_integrations/connectors/github_connection.py
+++ /dev/null
@@ -1,44 +0,0 @@
-import frappe
-from frappe.data_migration.doctype.data_migration_connector.connectors.base import BaseConnection
-from github import Github
-
-class GithubConnection(BaseConnection):
-	def __init__(self, connector):
-		self.connector = connector
-
-		try:
-			password = self.get_password()
-		except frappe.AuthenticationError:
-			password = None
-
-		if self.connector.username and password:
-			self.connection = Github(self.connector.username, self.get_password())
-		else:
-			self.connection = Github()
-
-		self.name_field = 'id'
-
-	def insert(self, doctype, doc):
-		pass
-
-	def update(self, doctype, doc, migration_id):
-		pass
-
-	def delete(self, doctype, migration_id):
-		pass
-
-	def get(self, remote_objectname, fields=None, filters=None, start=0, page_length=10):
-		repo = filters.get('repo')
-
-		if remote_objectname == 'Milestone':
-			return self.get_milestones(repo, start, page_length)
-		if remote_objectname == 'Issue':
-			return self.get_issues(repo, start, page_length)
-
-	def get_milestones(self, repo, start=0, page_length=10):
-		_repo = self.connection.get_repo(repo)
-		return list(_repo.get_milestones()[start:start+page_length])
-
-	def get_issues(self, repo, start=0, page_length=10):
-		_repo = self.connection.get_repo(repo)
-		return list(_repo.get_issues()[start:start+page_length])
diff --git a/erpnext/erpnext_integrations/data_migration_mapping/issue_to_task/__init__.py b/erpnext/erpnext_integrations/data_migration_mapping/issue_to_task/__init__.py
deleted file mode 100644
index 616ecfb..0000000
--- a/erpnext/erpnext_integrations/data_migration_mapping/issue_to_task/__init__.py
+++ /dev/null
@@ -1,12 +0,0 @@
-import frappe
-
-
-def pre_process(issue):
-
-	project = frappe.db.get_value("Project", filters={"project_name": issue.milestone})
-	return {
-		"title": issue.title,
-		"body": frappe.utils.md_to_html(issue.body or ""),
-		"state": issue.state.title(),
-		"project": project or "",
-	}
diff --git a/erpnext/erpnext_integrations/data_migration_mapping/issue_to_task/issue_to_task.json b/erpnext/erpnext_integrations/data_migration_mapping/issue_to_task/issue_to_task.json
deleted file mode 100644
index e945ba2..0000000
--- a/erpnext/erpnext_integrations/data_migration_mapping/issue_to_task/issue_to_task.json
+++ /dev/null
@@ -1,36 +0,0 @@
-{
- "condition": "{\"repo\":\"frappe/erpnext\"}", 
- "creation": "2017-10-16 16:03:32.772191", 
- "docstatus": 0, 
- "doctype": "Data Migration Mapping", 
- "fields": [
-  {
-   "is_child_table": 0, 
-   "local_fieldname": "subject", 
-   "remote_fieldname": "title"
-  }, 
-  {
-   "is_child_table": 0, 
-   "local_fieldname": "description", 
-   "remote_fieldname": "body"
-  }, 
-  {
-   "is_child_table": 0, 
-   "local_fieldname": "status", 
-   "remote_fieldname": "state"
-  }
- ], 
- "idx": 0, 
- "local_doctype": "Task", 
- "local_primary_key": "name", 
- "mapping_name": "Issue to Task", 
- "mapping_type": "Pull", 
- "migration_id_field": "github_sync_id", 
- "modified": "2017-10-20 11:48:54.575993", 
- "modified_by": "Administrator", 
- "name": "Issue to Task", 
- "owner": "Administrator", 
- "page_length": 10, 
- "remote_objectname": "Issue", 
- "remote_primary_key": "id"
-}
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/data_migration_mapping/milestone_to_project/__init__.py b/erpnext/erpnext_integrations/data_migration_mapping/milestone_to_project/__init__.py
deleted file mode 100644
index d44fc04..0000000
--- a/erpnext/erpnext_integrations/data_migration_mapping/milestone_to_project/__init__.py
+++ /dev/null
@@ -1,6 +0,0 @@
-def pre_process(milestone):
-	return {
-		"title": milestone.title,
-		"description": milestone.description,
-		"state": milestone.state.title(),
-	}
diff --git a/erpnext/erpnext_integrations/data_migration_mapping/milestone_to_project/milestone_to_project.json b/erpnext/erpnext_integrations/data_migration_mapping/milestone_to_project/milestone_to_project.json
deleted file mode 100644
index 5a3e07e..0000000
--- a/erpnext/erpnext_integrations/data_migration_mapping/milestone_to_project/milestone_to_project.json
+++ /dev/null
@@ -1,36 +0,0 @@
-{
- "condition": "{\"repo\": \"frappe/erpnext\"}", 
- "creation": "2017-10-13 11:16:49.664925", 
- "docstatus": 0, 
- "doctype": "Data Migration Mapping", 
- "fields": [
-  {
-   "is_child_table": 0, 
-   "local_fieldname": "project_name", 
-   "remote_fieldname": "title"
-  }, 
-  {
-   "is_child_table": 0, 
-   "local_fieldname": "notes", 
-   "remote_fieldname": "description"
-  }, 
-  {
-   "is_child_table": 0, 
-   "local_fieldname": "status", 
-   "remote_fieldname": "state"
-  }
- ], 
- "idx": 0, 
- "local_doctype": "Project", 
- "local_primary_key": "project_name", 
- "mapping_name": "Milestone to Project", 
- "mapping_type": "Pull", 
- "migration_id_field": "github_sync_id", 
- "modified": "2017-10-20 11:48:54.552305", 
- "modified_by": "Administrator", 
- "name": "Milestone to Project", 
- "owner": "Administrator", 
- "page_length": 10, 
- "remote_objectname": "Milestone", 
- "remote_primary_key": "id"
-}
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/data_migration_plan/github_sync/github_sync.json b/erpnext/erpnext_integrations/data_migration_plan/github_sync/github_sync.json
deleted file mode 100644
index 20eb387..0000000
--- a/erpnext/erpnext_integrations/data_migration_plan/github_sync/github_sync.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "creation": "2017-10-13 11:16:53.600026", 
- "docstatus": 0, 
- "doctype": "Data Migration Plan", 
- "idx": 0, 
- "mappings": [
-  {
-   "enabled": 1, 
-   "mapping": "Milestone to Project"
-  }, 
-  {
-   "enabled": 1, 
-   "mapping": "Issue to Task"
-  }
- ], 
- "modified": "2017-10-20 11:48:54.496123", 
- "modified_by": "Administrator", 
- "module": "ERPNext Integrations", 
- "name": "GitHub Sync", 
- "owner": "Administrator", 
- "plan_name": "GitHub Sync"
-}
\ No newline at end of file
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 1c4bbbc..7d7f65d 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -392,9 +392,12 @@
 
 scheduler_events = {
 	"cron": {
+		"0/5 * * * *": [
+			"erpnext.manufacturing.doctype.bom_update_log.bom_update_log.resume_bom_cost_update_jobs",
+		],
 		"0/30 * * * *": [
 			"erpnext.utilities.doctype.video.video.update_youtube_data",
-		]
+		],
 	},
 	"all": [
 		"erpnext.projects.doctype.project.project.project_status_update_reminder",
diff --git a/erpnext/hr/doctype/attendance/test_attendance.py b/erpnext/hr/doctype/attendance/test_attendance.py
index 762d0f7..c85ec65 100644
--- a/erpnext/hr/doctype/attendance/test_attendance.py
+++ b/erpnext/hr/doctype/attendance/test_attendance.py
@@ -3,7 +3,15 @@
 
 import frappe
 from frappe.tests.utils import FrappeTestCase
-from frappe.utils import add_days, get_year_ending, get_year_start, getdate, now_datetime, nowdate
+from frappe.utils import (
+	add_days,
+	add_months,
+	get_last_day,
+	get_year_ending,
+	get_year_start,
+	getdate,
+	nowdate,
+)
 
 from erpnext.hr.doctype.attendance.attendance import (
 	DuplicateAttendanceError,
@@ -138,69 +146,70 @@
 		self.assertEqual(attendance, fetch_attendance)
 
 	def test_unmarked_days(self):
-		now = now_datetime()
-		previous_month = now.month - 1
-		first_day = now.replace(day=1).replace(month=previous_month).date()
+		first_sunday = get_first_sunday(
+			self.holiday_list, for_date=get_last_day(add_months(getdate(), -1))
+		)
+		attendance_date = add_days(first_sunday, 1)
 
 		employee = make_employee(
-			"test_unmarked_days@example.com", date_of_joining=add_days(first_day, -1)
+			"test_unmarked_days@example.com", date_of_joining=add_days(attendance_date, -1)
 		)
 		frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list)
 
-		first_sunday = get_first_sunday(self.holiday_list, for_date=first_day)
-		mark_attendance(employee, first_day, "Present")
-		month_name = get_month_name(first_day)
+		mark_attendance(employee, attendance_date, "Present")
+		month_name = get_month_name(attendance_date)
 
 		unmarked_days = get_unmarked_days(employee, month_name)
 		unmarked_days = [getdate(date) for date in unmarked_days]
 
 		# attendance already marked for the day
-		self.assertNotIn(first_day, unmarked_days)
+		self.assertNotIn(attendance_date, unmarked_days)
 		# attendance unmarked
-		self.assertIn(getdate(add_days(first_day, 1)), unmarked_days)
+		self.assertIn(getdate(add_days(attendance_date, 1)), unmarked_days)
 		# holiday considered in unmarked days
 		self.assertIn(first_sunday, unmarked_days)
 
 	def test_unmarked_days_excluding_holidays(self):
-		now = now_datetime()
-		previous_month = now.month - 1
-		first_day = now.replace(day=1).replace(month=previous_month).date()
+		first_sunday = get_first_sunday(
+			self.holiday_list, for_date=get_last_day(add_months(getdate(), -1))
+		)
+		attendance_date = add_days(first_sunday, 1)
 
 		employee = make_employee(
-			"test_unmarked_days@example.com", date_of_joining=add_days(first_day, -1)
+			"test_unmarked_days@example.com", date_of_joining=add_days(attendance_date, -1)
 		)
 		frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list)
 
-		first_sunday = get_first_sunday(self.holiday_list, for_date=first_day)
-		mark_attendance(employee, first_day, "Present")
-		month_name = get_month_name(first_day)
+		mark_attendance(employee, attendance_date, "Present")
+		month_name = get_month_name(attendance_date)
 
 		unmarked_days = get_unmarked_days(employee, month_name, exclude_holidays=True)
 		unmarked_days = [getdate(date) for date in unmarked_days]
 
 		# attendance already marked for the day
-		self.assertNotIn(first_day, unmarked_days)
+		self.assertNotIn(attendance_date, unmarked_days)
 		# attendance unmarked
-		self.assertIn(getdate(add_days(first_day, 1)), unmarked_days)
+		self.assertIn(getdate(add_days(attendance_date, 1)), unmarked_days)
 		# holidays not considered in unmarked days
 		self.assertNotIn(first_sunday, unmarked_days)
 
 	def test_unmarked_days_as_per_joining_and_relieving_dates(self):
-		now = now_datetime()
-		previous_month = now.month - 1
-		first_day = now.replace(day=1).replace(month=previous_month).date()
+		first_sunday = get_first_sunday(
+			self.holiday_list, for_date=get_last_day(add_months(getdate(), -1))
+		)
+		date = add_days(first_sunday, 1)
 
-		doj = add_days(first_day, 1)
-		relieving_date = add_days(first_day, 5)
+		doj = add_days(date, 1)
+		relieving_date = add_days(date, 5)
 		employee = make_employee(
 			"test_unmarked_days_as_per_doj@example.com", date_of_joining=doj, relieving_date=relieving_date
 		)
 
 		frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list)
 
-		attendance_date = add_days(first_day, 2)
+		attendance_date = add_days(date, 2)
 		mark_attendance(employee, attendance_date, "Present")
-		month_name = get_month_name(first_day)
+		month_name = get_month_name(attendance_date)
 
 		unmarked_days = get_unmarked_days(employee, month_name)
 		unmarked_days = [getdate(date) for date in unmarked_days]
diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json
index a3638e1..4247914 100644
--- a/erpnext/hr/doctype/employee/employee.json
+++ b/erpnext/hr/doctype/employee/employee.json
@@ -827,7 +827,7 @@
  "idx": 24,
  "image_field": "image",
  "links": [],
- "modified": "2022-04-22 16:21:55.811983",
+ "modified": "2022-06-10 01:29:32.952091",
  "modified_by": "Administrator",
  "module": "HR",
  "name": "Employee",
@@ -872,7 +872,6 @@
  ],
  "search_fields": "employee_name",
  "show_name_in_global_search": 1,
- "show_title_field_in_link": 1,
  "sort_field": "modified",
  "sort_order": "DESC",
  "states": [],
diff --git a/erpnext/hr/doctype/employee/employee_reminders.py b/erpnext/hr/doctype/employee/employee_reminders.py
index 1829bc4..f09d7ff 100644
--- a/erpnext/hr/doctype/employee/employee_reminders.py
+++ b/erpnext/hr/doctype/employee/employee_reminders.py
@@ -230,7 +230,7 @@
 		persons_name = anniversary_person
 		# Number of years completed at the company
 		completed_years = getdate().year - anniversary_persons[0]["date_of_joining"].year
-		anniversary_person += f" completed {completed_years} year(s)"
+		anniversary_person += f" completed {get_pluralized_years(completed_years)}"
 	else:
 		person_names_with_years = []
 		names = []
@@ -239,7 +239,7 @@
 			names.append(person_text)
 			# Number of years completed at the company
 			completed_years = getdate().year - person["date_of_joining"].year
-			person_text += f" completed {completed_years} year(s)"
+			person_text += f" completed {get_pluralized_years(completed_years)}"
 			person_names_with_years.append(person_text)
 
 		# converts ["Jim", "Rim", "Dim"] to Jim, Rim & Dim
@@ -254,6 +254,12 @@
 	return reminder_text, message
 
 
+def get_pluralized_years(years):
+	if years == 1:
+		return "1 year"
+	return f"{years} years"
+
+
 def send_work_anniversary_reminder(recipients, reminder_text, anniversary_persons, message):
 	frappe.sendmail(
 		recipients=recipients,
diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.py b/erpnext/hr/doctype/hr_settings/hr_settings.py
index 72a49e2..b56f3db 100644
--- a/erpnext/hr/doctype/hr_settings/hr_settings.py
+++ b/erpnext/hr/doctype/hr_settings/hr_settings.py
@@ -22,7 +22,7 @@
 		PROCEED_WITH_FREQUENCY_CHANGE = False
 
 	def set_naming_series(self):
-		from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series
+		from erpnext.utilities.naming import set_by_naming_series
 
 		set_by_naming_series(
 			"Employee",
diff --git a/erpnext/hr/doctype/leave_application/leave_application.js b/erpnext/hr/doctype/leave_application/leave_application.js
index 85997a4..ee00e67 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.js
+++ b/erpnext/hr/doctype/leave_application/leave_application.js
@@ -173,7 +173,7 @@
 					date: frm.doc.from_date,
 					to_date: frm.doc.to_date,
 					leave_type: frm.doc.leave_type,
-					consider_all_leaves_in_the_allocation_period: true
+					consider_all_leaves_in_the_allocation_period: 1
 				},
 				callback: function (r) {
 					if (!r.exc && r.message) {
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index cd6b168..43c2bb3 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -88,7 +88,7 @@
 		share_doc_with_approver(self, self.leave_approver)
 
 	def on_submit(self):
-		if self.status == "Open":
+		if self.status in ["Open", "Cancelled"]:
 			frappe.throw(
 				_("Only Leave Applications with status 'Approved' and 'Rejected' can be submitted")
 			)
@@ -757,22 +757,6 @@
 	leave_allocation = {}
 	for d in allocation_records:
 		allocation = allocation_records.get(d, frappe._dict())
-
-		total_allocated_leaves = (
-			frappe.db.get_value(
-				"Leave Allocation",
-				{
-					"from_date": ("<=", date),
-					"to_date": (">=", date),
-					"employee": employee,
-					"leave_type": allocation.leave_type,
-					"docstatus": 1,
-				},
-				"SUM(total_leaves_allocated)",
-			)
-			or 0
-		)
-
 		remaining_leaves = get_leave_balance_on(
 			employee, d, date, to_date=allocation.to_date, consider_all_leaves_in_the_allocation_period=True
 		)
@@ -782,10 +766,11 @@
 		leaves_pending = get_leaves_pending_approval_for_period(
 			employee, d, allocation.from_date, end_date
 		)
+		expired_leaves = allocation.total_leaves_allocated - (remaining_leaves + leaves_taken)
 
 		leave_allocation[d] = {
-			"total_leaves": total_allocated_leaves,
-			"expired_leaves": total_allocated_leaves - (remaining_leaves + leaves_taken),
+			"total_leaves": allocation.total_leaves_allocated,
+			"expired_leaves": expired_leaves if expired_leaves > 0 else 0,
 			"leaves_taken": leaves_taken,
 			"leaves_pending_approval": leaves_pending,
 			"remaining_leaves": remaining_leaves,
@@ -830,7 +815,7 @@
 	allocation_records = get_leave_allocation_records(employee, date, leave_type)
 	allocation = allocation_records.get(leave_type, frappe._dict())
 
-	end_date = allocation.to_date if consider_all_leaves_in_the_allocation_period else date
+	end_date = allocation.to_date if cint(consider_all_leaves_in_the_allocation_period) else date
 	cf_expiry = get_allocation_expiry_for_cf_leaves(employee, leave_type, to_date, date)
 
 	leaves_taken = get_leaves_for_period(employee, leave_type, allocation.from_date, end_date)
@@ -1117,7 +1102,7 @@
 	WHERE
 		from_date <= %(end)s AND to_date >= %(start)s <= to_date
 		AND docstatus < 2
-		AND status != 'Rejected'
+		AND status in ('Approved', 'Open')
 	"""
 
 	if conditions:
@@ -1201,24 +1186,33 @@
 
 
 def get_approved_leaves_for_period(employee, leave_type, from_date, to_date):
-	query = """
-		select employee, leave_type, from_date, to_date, total_leave_days
-		from `tabLeave Application`
-		where employee=%(employee)s
-			and docstatus=1
-			and (from_date between %(from_date)s and %(to_date)s
-				or to_date between %(from_date)s and %(to_date)s
-				or (from_date < %(from_date)s and to_date > %(to_date)s))
-	"""
-	if leave_type:
-		query += "and leave_type=%(leave_type)s"
-
-	leave_applications = frappe.db.sql(
-		query,
-		{"from_date": from_date, "to_date": to_date, "employee": employee, "leave_type": leave_type},
-		as_dict=1,
+	LeaveApplication = frappe.qb.DocType("Leave Application")
+	query = (
+		frappe.qb.from_(LeaveApplication)
+		.select(
+			LeaveApplication.employee,
+			LeaveApplication.leave_type,
+			LeaveApplication.from_date,
+			LeaveApplication.to_date,
+			LeaveApplication.total_leave_days,
+		)
+		.where(
+			(LeaveApplication.employee == employee)
+			& (LeaveApplication.docstatus == 1)
+			& (LeaveApplication.status == "Approved")
+			& (
+				(LeaveApplication.from_date.between(from_date, to_date))
+				| (LeaveApplication.to_date.between(from_date, to_date))
+				| ((LeaveApplication.from_date < from_date) & (LeaveApplication.to_date > to_date))
+			)
+		)
 	)
 
+	if leave_type:
+		query = query.where(LeaveApplication.leave_type == leave_type)
+
+	leave_applications = query.run(as_dict=True)
+
 	leave_days = 0
 	for leave_app in leave_applications:
 		if leave_app.from_date >= getdate(from_date) and leave_app.to_date <= getdate(to_date):
diff --git a/erpnext/hr/doctype/leave_application/leave_application_list.js b/erpnext/hr/doctype/leave_application/leave_application_list.js
index a3c03b1..157271a 100644
--- a/erpnext/hr/doctype/leave_application/leave_application_list.js
+++ b/erpnext/hr/doctype/leave_application/leave_application_list.js
@@ -1,13 +1,14 @@
-frappe.listview_settings['Leave Application'] = {
+frappe.listview_settings["Leave Application"] = {
 	add_fields: ["leave_type", "employee", "employee_name", "total_leave_days", "from_date", "to_date"],
 	has_indicator_for_draft: 1,
 	get_indicator: function (doc) {
-		if (doc.status === "Approved") {
-			return [__("Approved"), "green", "status,=,Approved"];
-		} else if (doc.status === "Rejected") {
-			return [__("Rejected"), "red", "status,=,Rejected"];
-		} else {
-			return [__("Open"), "red", "status,=,Open"];
-		}
+		let status_color = {
+			"Approved": "green",
+			"Rejected": "red",
+			"Open": "orange",
+			"Cancelled": "red",
+			"Submitted": "blue"
+		};
+		return [__(doc.status), status_color[doc.status], "status,=," + doc.status];
 	}
 };
diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py
index 7506c61..27c5410 100644
--- a/erpnext/hr/doctype/leave_application/test_leave_application.py
+++ b/erpnext/hr/doctype/leave_application/test_leave_application.py
@@ -76,7 +76,14 @@
 
 class TestLeaveApplication(unittest.TestCase):
 	def setUp(self):
-		for dt in ["Leave Application", "Leave Allocation", "Salary Slip", "Leave Ledger Entry"]:
+		for dt in [
+			"Leave Application",
+			"Leave Allocation",
+			"Salary Slip",
+			"Leave Ledger Entry",
+			"Leave Period",
+			"Leave Policy Assignment",
+		]:
 			frappe.db.delete(dt)
 
 		frappe.set_user("Administrator")
@@ -702,59 +709,24 @@
 		self.assertEqual(details.leave_balance, 30)
 
 	def test_earned_leaves_creation(self):
-
-		frappe.db.sql("""delete from `tabLeave Period`""")
-		frappe.db.sql("""delete from `tabLeave Policy Assignment`""")
-		frappe.db.sql("""delete from `tabLeave Allocation`""")
-		frappe.db.sql("""delete from `tabLeave Ledger Entry`""")
+		from erpnext.hr.utils import allocate_earned_leaves
 
 		leave_period = get_leave_period()
 		employee = get_employee()
 		leave_type = "Test Earned Leave Type"
-		frappe.delete_doc_if_exists("Leave Type", "Test Earned Leave Type", force=1)
-		frappe.get_doc(
-			dict(
-				leave_type_name=leave_type,
-				doctype="Leave Type",
-				is_earned_leave=1,
-				earned_leave_frequency="Monthly",
-				rounding=0.5,
-				max_leaves_allowed=6,
-			)
-		).insert()
+		make_policy_assignment(employee, leave_type, leave_period)
 
-		leave_policy = frappe.get_doc(
-			{
-				"doctype": "Leave Policy",
-				"title": "Test Leave Policy",
-				"leave_policy_details": [{"leave_type": leave_type, "annual_allocation": 6}],
-			}
-		).insert()
-
-		data = {
-			"assignment_based_on": "Leave Period",
-			"leave_policy": leave_policy.name,
-			"leave_period": leave_period.name,
-		}
-
-		leave_policy_assignments = create_assignment_for_multiple_employees(
-			[employee.name], frappe._dict(data)
-		)
-
-		from erpnext.hr.utils import allocate_earned_leaves
-
-		i = 0
-		while i < 14:
+		for i in range(0, 14):
 			allocate_earned_leaves()
-			i += 1
+
 		self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 6)
 
 		# validate earned leaves creation without maximum leaves
 		frappe.db.set_value("Leave Type", leave_type, "max_leaves_allowed", 0)
-		i = 0
-		while i < 6:
+
+		for i in range(0, 6):
 			allocate_earned_leaves()
-			i += 1
+
 		self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 9)
 
 	# test to not consider current leave in leave balance while submitting
@@ -971,6 +943,54 @@
 		self.assertEqual(leave_allocation["remaining_leaves"], 26)
 
 	@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
+	def test_get_earned_leave_details_for_dashboard(self):
+		from erpnext.hr.utils import allocate_earned_leaves
+
+		leave_period = get_leave_period()
+		employee = get_employee()
+		leave_type = "Test Earned Leave Type"
+		leave_policy_assignments = make_policy_assignment(employee, leave_type, leave_period)
+		allocation = frappe.db.get_value(
+			"Leave Allocation",
+			{"leave_policy_assignment": leave_policy_assignments[0]},
+			"name",
+		)
+		allocation = frappe.get_doc("Leave Allocation", allocation)
+		allocation.new_leaves_allocated = 2
+		allocation.save()
+
+		for i in range(0, 6):
+			allocate_earned_leaves()
+
+		first_sunday = get_first_sunday(self.holiday_list)
+		make_leave_application(
+			employee.name, add_days(first_sunday, 1), add_days(first_sunday, 1), leave_type
+		)
+
+		details = get_leave_details(employee.name, allocation.from_date)
+		leave_allocation = details["leave_allocation"][leave_type]
+		expected = {
+			"total_leaves": 2.0,
+			"expired_leaves": 0.0,
+			"leaves_taken": 1.0,
+			"leaves_pending_approval": 0.0,
+			"remaining_leaves": 1.0,
+		}
+		self.assertEqual(leave_allocation, expected)
+
+		details = get_leave_details(employee.name, getdate())
+		leave_allocation = details["leave_allocation"][leave_type]
+
+		expected = {
+			"total_leaves": 5.0,
+			"expired_leaves": 0.0,
+			"leaves_taken": 1.0,
+			"leaves_pending_approval": 0.0,
+			"remaining_leaves": 4.0,
+		}
+		self.assertEqual(leave_allocation, expected)
+
+	@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
 	def test_get_leave_allocation_records(self):
 		employee = get_employee()
 		leave_type = create_leave_type(
@@ -1100,3 +1120,36 @@
 	)[0][0]
 
 	return first_sunday
+
+
+def make_policy_assignment(employee, leave_type, leave_period):
+	frappe.delete_doc_if_exists("Leave Type", leave_type, force=1)
+	frappe.get_doc(
+		dict(
+			leave_type_name=leave_type,
+			doctype="Leave Type",
+			is_earned_leave=1,
+			earned_leave_frequency="Monthly",
+			rounding=0.5,
+			max_leaves_allowed=6,
+		)
+	).insert()
+
+	leave_policy = frappe.get_doc(
+		{
+			"doctype": "Leave Policy",
+			"title": "Test Leave Policy",
+			"leave_policy_details": [{"leave_type": leave_type, "annual_allocation": 6}],
+		}
+	).insert()
+
+	data = {
+		"assignment_based_on": "Leave Period",
+		"leave_policy": leave_policy.name,
+		"leave_period": leave_period.name,
+	}
+
+	leave_policy_assignments = create_assignment_for_multiple_employees(
+		[employee.name], frappe._dict(data)
+	)
+	return leave_policy_assignments
diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py
index 269e4aa..3f4e31b 100644
--- a/erpnext/hr/utils.py
+++ b/erpnext/hr/utils.py
@@ -55,6 +55,8 @@
 			new_data = getdate(new_data)
 		elif fieldtype == "Datetime" and new_data:
 			new_data = get_datetime(new_data)
+		elif fieldtype in ["Currency", "Float"] and new_data:
+			new_data = flt(new_data)
 		setattr(employee, item.fieldname, new_data)
 		if item.fieldname in ["department", "designation", "branch"]:
 			internal_work_history[item.fieldname] = item.new
@@ -439,20 +441,18 @@
 	return False
 
 
-def get_salary_assignment(employee, date):
-	assignment = frappe.db.sql(
-		"""
-		select * from `tabSalary Structure Assignment`
-		where employee=%(employee)s
-		and docstatus = 1
-		and %(on_date)s >= from_date order by from_date desc limit 1""",
-		{
-			"employee": employee,
-			"on_date": date,
-		},
-		as_dict=1,
+def get_salary_assignments(employee, payroll_period):
+	start_date, end_date = frappe.db.get_value(
+		"Payroll Period", payroll_period, ["start_date", "end_date"]
 	)
-	return assignment[0] if assignment else None
+	assignments = frappe.db.get_all(
+		"Salary Structure Assignment",
+		filters={"employee": employee, "docstatus": 1, "from_date": ["between", (start_date, end_date)]},
+		fields=["*"],
+		order_by="from_date",
+	)
+
+	return assignments
 
 
 def get_sal_slip_total_benefit_given(employee, payroll_period, component=False):
diff --git a/erpnext/loan_management/doctype/loan/loan.js b/erpnext/loan_management/doctype/loan/loan.js
index 940a1bb..38328e6 100644
--- a/erpnext/loan_management/doctype/loan/loan.js
+++ b/erpnext/loan_management/doctype/loan/loan.js
@@ -93,6 +93,12 @@
 					frm.trigger("make_loan_refund");
 				},__('Create'));
 			}
+
+			if (frm.doc.status == "Loan Closure Requested" && frm.doc.is_term_loan && !frm.doc.is_secured_loan) {
+				frm.add_custom_button(__('Close Loan'), function() {
+					frm.trigger("close_unsecured_term_loan");
+				},__('Status'));
+			}
 		}
 		frm.trigger("toggle_fields");
 	},
@@ -174,6 +180,18 @@
 		})
 	},
 
+	close_unsecured_term_loan: function(frm) {
+		frappe.call({
+			args: {
+				"loan": frm.doc.name
+			},
+			method: "erpnext.loan_management.doctype.loan.loan.close_unsecured_term_loan",
+			callback: function () {
+				frm.refresh();
+			}
+		})
+	},
+
 	request_loan_closure: function(frm) {
 		frappe.confirm(__("Do you really want to close this loan"),
 			function() {
diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py
index a0ef1b9..90ce004 100644
--- a/erpnext/loan_management/doctype/loan/loan.py
+++ b/erpnext/loan_management/doctype/loan/loan.py
@@ -60,18 +60,20 @@
 				)
 
 	def validate_cost_center(self):
-		if not self.cost_center and self.rate_of_interest != 0:
+		if not self.cost_center and self.rate_of_interest != 0.0:
 			self.cost_center = frappe.db.get_value("Company", self.company, "cost_center")
 
-		if not self.cost_center:
-			frappe.throw(_("Cost center is mandatory for loans having rate of interest greater than 0"))
+			if not self.cost_center:
+				frappe.throw(_("Cost center is mandatory for loans having rate of interest greater than 0"))
 
 	def on_submit(self):
 		self.link_loan_security_pledge()
+		# Interest accrual for backdated term loans
+		self.accrue_loan_interest()
 
 	def on_cancel(self):
 		self.unlink_loan_security_pledge()
-		self.ignore_linked_doctypes = ["GL Entry"]
+		self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
 
 	def set_missing_fields(self):
 		if not self.company:
@@ -187,6 +189,16 @@
 
 				self.db_set("maximum_loan_amount", maximum_loan_value)
 
+	def accrue_loan_interest(self):
+		from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import (
+			process_loan_interest_accrual_for_term_loans,
+		)
+
+		if getdate(self.repayment_start_date) < getdate() and self.is_term_loan:
+			process_loan_interest_accrual_for_term_loans(
+				posting_date=getdate(), loan_type=self.loan_type, loan=self.name
+			)
+
 	def unlink_loan_security_pledge(self):
 		pledges = frappe.get_all("Loan Security Pledge", fields=["name"], filters={"loan": self.name})
 		pledge_list = [d.name for d in pledges]
@@ -330,6 +342,22 @@
 		return loan.as_dict()
 
 
+@frappe.whitelist()
+def close_unsecured_term_loan(loan):
+	loan_details = frappe.db.get_value(
+		"Loan", {"name": loan}, ["status", "is_term_loan", "is_secured_loan"], as_dict=1
+	)
+
+	if (
+		loan_details.status == "Loan Closure Requested"
+		and loan_details.is_term_loan
+		and not loan_details.is_secured_loan
+	):
+		frappe.db.set_value("Loan", loan, "status", "Closed")
+	else:
+		frappe.throw(_("Cannot close this loan until full repayment"))
+
+
 def close_loan(loan, total_amount_paid):
 	frappe.db.set_value("Loan", loan, "total_amount_paid", total_amount_paid)
 	frappe.db.set_value("Loan", loan, "status", "Closed")
diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
index 10174e5..0c2042b 100644
--- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
+++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
@@ -29,7 +29,7 @@
 	def on_cancel(self):
 		self.set_status_and_amounts(cancel=1)
 		self.make_gl_entries(cancel=1)
-		self.ignore_linked_doctypes = ["GL Entry"]
+		self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
 
 	def set_missing_values(self):
 		if not self.disbursement_date:
diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py
index 3a4c651..0aeb448 100644
--- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py
+++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py
@@ -32,7 +32,7 @@
 			self.update_is_accrued()
 
 		self.make_gl_entries(cancel=1)
-		self.ignore_linked_doctypes = ["GL Entry"]
+		self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
 
 	def update_is_accrued(self):
 		frappe.db.set_value("Repayment Schedule", self.repayment_schedule_name, "is_accrued", 0)
diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
index dcbdf8a..51f40d9 100644
--- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
+++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
@@ -41,7 +41,7 @@
 		self.check_future_accruals()
 		self.update_repayment_schedule(cancel=1)
 		self.mark_as_unpaid()
-		self.ignore_linked_doctypes = ["GL Entry"]
+		self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
 		self.make_gl_entries(cancel=1)
 
 	def set_missing_values(self, amounts):
diff --git a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py
index e19fd15..25aecf6 100644
--- a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py
+++ b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py
@@ -42,7 +42,7 @@
 
 	def on_cancel(self):
 		self.update_outstanding_amount(cancel=1)
-		self.ignore_linked_doctypes = ["GL Entry"]
+		self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
 		self.make_gl_entries(cancel=1)
 
 	def update_outstanding_amount(self, cancel=0):
diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js
index d743798..ecad41f 100644
--- a/erpnext/manufacturing/doctype/bom/bom.js
+++ b/erpnext/manufacturing/doctype/bom/bom.js
@@ -81,7 +81,7 @@
 			}
 		)
 
-		if (!frm.doc.__islocal && frm.doc.docstatus<2) {
+		if (!frm.is_new() && frm.doc.docstatus<2) {
 			frm.add_custom_button(__("Update Cost"), function() {
 				frm.events.update_cost(frm, true);
 			});
@@ -93,10 +93,12 @@
 			});
 		}
 
-		frm.add_custom_button(__("New Version"), function() {
-			let new_bom = frappe.model.copy_doc(frm.doc);
-			frappe.set_route("Form", "BOM", new_bom.name);
-		});
+		if (!frm.is_new() && !frm.doc.docstatus == 0) {
+			frm.add_custom_button(__("New Version"), function() {
+				let new_bom = frappe.model.copy_doc(frm.doc);
+				frappe.set_route("Form", "BOM", new_bom.name);
+			});
+		}
 
 		if(frm.doc.docstatus==1) {
 			frm.add_custom_button(__("Work Order"), function() {
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 6376359..631548b 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -1,11 +1,11 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
 # License: GNU General Public License v3. See license.txt
 
 import functools
 import re
 from collections import deque
 from operator import itemgetter
-from typing import List
+from typing import Dict, List
 
 import frappe
 from frappe import _
@@ -189,6 +189,7 @@
 		self.validate_transfer_against()
 		self.set_routing_operations()
 		self.validate_operations()
+		self.update_exploded_items(save=False)
 		self.calculate_cost()
 		self.update_stock_qty()
 		self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate=False, save=False)
@@ -386,40 +387,14 @@
 
 		existing_bom_cost = self.total_cost
 
-		for d in self.get("items"):
-			if not d.item_code:
-				continue
-
-			rate = self.get_rm_rate(
-				{
-					"company": self.company,
-					"item_code": d.item_code,
-					"bom_no": d.bom_no,
-					"qty": d.qty,
-					"uom": d.uom,
-					"stock_uom": d.stock_uom,
-					"conversion_factor": d.conversion_factor,
-					"sourced_by_supplier": d.sourced_by_supplier,
-				}
-			)
-
-			if rate:
-				d.rate = rate
-			d.amount = flt(d.rate) * flt(d.qty)
-			d.base_rate = flt(d.rate) * flt(self.conversion_rate)
-			d.base_amount = flt(d.amount) * flt(self.conversion_rate)
-
-			if save:
-				d.db_update()
-
 		if self.docstatus == 1:
 			self.flags.ignore_validate_update_after_submit = True
-			self.calculate_cost(update_hour_rate)
+
+		self.calculate_cost(save_updates=save, update_hour_rate=update_hour_rate)
+
 		if save:
 			self.db_update()
 
-		self.update_exploded_items(save=save)
-
 		# update parent BOMs
 		if self.total_cost != existing_bom_cost and update_parent:
 			parent_boms = frappe.db.sql_list(
@@ -608,11 +583,15 @@
 		bom_list.reverse()
 		return bom_list
 
-	def calculate_cost(self, update_hour_rate=False):
+	def calculate_cost(self, save_updates=False, update_hour_rate=False):
 		"""Calculate bom totals"""
 		self.calculate_op_cost(update_hour_rate)
-		self.calculate_rm_cost()
-		self.calculate_sm_cost()
+		self.calculate_rm_cost(save=save_updates)
+		self.calculate_sm_cost(save=save_updates)
+		if save_updates:
+			# not via doc event, table is not regenerated and needs updation
+			self.calculate_exploded_cost()
+
 		self.total_cost = self.operating_cost + self.raw_material_cost - self.scrap_material_cost
 		self.base_total_cost = (
 			self.base_operating_cost + self.base_raw_material_cost - self.base_scrap_material_cost
@@ -654,12 +633,26 @@
 		if update_hour_rate:
 			row.db_update()
 
-	def calculate_rm_cost(self):
+	def calculate_rm_cost(self, save=False):
 		"""Fetch RM rate as per today's valuation rate and calculate totals"""
 		total_rm_cost = 0
 		base_total_rm_cost = 0
 
 		for d in self.get("items"):
+			old_rate = d.rate
+			d.rate = self.get_rm_rate(
+				{
+					"company": self.company,
+					"item_code": d.item_code,
+					"bom_no": d.bom_no,
+					"qty": d.qty,
+					"uom": d.uom,
+					"stock_uom": d.stock_uom,
+					"conversion_factor": d.conversion_factor,
+					"sourced_by_supplier": d.sourced_by_supplier,
+				}
+			)
+
 			d.base_rate = flt(d.rate) * flt(self.conversion_rate)
 			d.amount = flt(d.rate, d.precision("rate")) * flt(d.qty, d.precision("qty"))
 			d.base_amount = d.amount * flt(self.conversion_rate)
@@ -669,11 +662,13 @@
 
 			total_rm_cost += d.amount
 			base_total_rm_cost += d.base_amount
+			if save and (old_rate != d.rate):
+				d.db_update()
 
 		self.raw_material_cost = total_rm_cost
 		self.base_raw_material_cost = base_total_rm_cost
 
-	def calculate_sm_cost(self):
+	def calculate_sm_cost(self, save=False):
 		"""Fetch RM rate as per today's valuation rate and calculate totals"""
 		total_sm_cost = 0
 		base_total_sm_cost = 0
@@ -688,10 +683,45 @@
 			)
 			total_sm_cost += d.amount
 			base_total_sm_cost += d.base_amount
+			if save:
+				d.db_update()
 
 		self.scrap_material_cost = total_sm_cost
 		self.base_scrap_material_cost = base_total_sm_cost
 
+	def calculate_exploded_cost(self):
+		"Set exploded row cost from it's parent BOM."
+		rm_rate_map = self.get_rm_rate_map()
+
+		for row in self.get("exploded_items"):
+			old_rate = flt(row.rate)
+			row.rate = rm_rate_map.get(row.item_code)
+			row.amount = flt(row.stock_qty) * flt(row.rate)
+
+			if old_rate != row.rate:
+				# Only db_update if changed
+				row.db_update()
+
+	def get_rm_rate_map(self) -> Dict[str, float]:
+		"Create Raw Material-Rate map for Exploded Items. Fetch rate from Items table or Subassembly BOM."
+		rm_rate_map = {}
+
+		for item in self.get("items"):
+			if item.bom_no:
+				# Get Item-Rate from Subassembly BOM
+				explosion_items = frappe.get_all(
+					"BOM Explosion Item",
+					filters={"parent": item.bom_no},
+					fields=["item_code", "rate"],
+					order_by=None,  # to avoid sort index creation at db level (granular change)
+				)
+				explosion_item_rate = {item.item_code: flt(item.rate) for item in explosion_items}
+				rm_rate_map.update(explosion_item_rate)
+			else:
+				rm_rate_map[item.item_code] = flt(item.base_rate) / flt(item.conversion_factor or 1.0)
+
+		return rm_rate_map
+
 	def update_exploded_items(self, save=True):
 		"""Update Flat BOM, following will be correct data"""
 		self.get_exploded_items()
@@ -902,44 +932,46 @@
 	return flt(rate)
 
 
-def get_valuation_rate(args):
-	"""Get weighted average of valuation rate from all warehouses"""
+def get_valuation_rate(data):
+	"""
+	1) Get average valuation rate from all warehouses
+	2) If no value, get last valuation rate from SLE
+	3) If no value, get valuation rate from Item
+	"""
+	from frappe.query_builder.functions import Sum
 
-	total_qty, total_value, valuation_rate = 0.0, 0.0, 0.0
-	item_bins = frappe.db.sql(
-		"""
-		select
-			bin.actual_qty, bin.stock_value
-		from
-			`tabBin` bin, `tabWarehouse` warehouse
-		where
-			bin.item_code=%(item)s
-			and bin.warehouse = warehouse.name
-			and warehouse.company=%(company)s""",
-		{"item": args["item_code"], "company": args["company"]},
-		as_dict=1,
-	)
+	item_code, company = data.get("item_code"), data.get("company")
+	valuation_rate = 0.0
 
-	for d in item_bins:
-		total_qty += flt(d.actual_qty)
-		total_value += flt(d.stock_value)
+	bin_table = frappe.qb.DocType("Bin")
+	wh_table = frappe.qb.DocType("Warehouse")
+	item_valuation = (
+		frappe.qb.from_(bin_table)
+		.join(wh_table)
+		.on(bin_table.warehouse == wh_table.name)
+		.select((Sum(bin_table.stock_value) / Sum(bin_table.actual_qty)).as_("valuation_rate"))
+		.where((bin_table.item_code == item_code) & (wh_table.company == company))
+	).run(as_dict=True)[0]
 
-	if total_qty:
-		valuation_rate = total_value / total_qty
+	valuation_rate = item_valuation.get("valuation_rate")
 
-	if valuation_rate <= 0:
-		last_valuation_rate = frappe.db.sql(
-			"""select valuation_rate
-			from `tabStock Ledger Entry`
-			where item_code = %s and valuation_rate > 0 and is_cancelled = 0
-			order by posting_date desc, posting_time desc, creation desc limit 1""",
-			args["item_code"],
-		)
+	if (valuation_rate is not None) and valuation_rate <= 0:
+		# Explicit null value check. If None, Bins don't exist, neither does SLE
+		sle = frappe.qb.DocType("Stock Ledger Entry")
+		last_val_rate = (
+			frappe.qb.from_(sle)
+			.select(sle.valuation_rate)
+			.where((sle.item_code == item_code) & (sle.valuation_rate > 0) & (sle.is_cancelled == 0))
+			.orderby(sle.posting_date, order=frappe.qb.desc)
+			.orderby(sle.posting_time, order=frappe.qb.desc)
+			.orderby(sle.creation, order=frappe.qb.desc)
+			.limit(1)
+		).run(as_dict=True)
 
-		valuation_rate = flt(last_valuation_rate[0][0]) if last_valuation_rate else 0
+		valuation_rate = flt(last_val_rate[0].get("valuation_rate")) if last_val_rate else 0
 
 	if not valuation_rate:
-		valuation_rate = frappe.db.get_value("Item", args["item_code"], "valuation_rate")
+		valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate")
 
 	return flt(valuation_rate)
 
@@ -1125,39 +1157,6 @@
 		return bom_items
 
 
-def get_boms_in_bottom_up_order(bom_no=None):
-	def _get_parent(bom_no):
-		return frappe.db.sql_list(
-			"""
-			select distinct bom_item.parent from `tabBOM Item` bom_item
-			where bom_item.bom_no = %s and bom_item.docstatus=1 and bom_item.parenttype='BOM'
-				and exists(select bom.name from `tabBOM` bom where bom.name=bom_item.parent and bom.is_active=1)
-		""",
-			bom_no,
-		)
-
-	count = 0
-	bom_list = []
-	if bom_no:
-		bom_list.append(bom_no)
-	else:
-		# get all leaf BOMs
-		bom_list = frappe.db.sql_list(
-			"""select name from `tabBOM` bom
-			where docstatus=1 and is_active=1
-				and not exists(select bom_no from `tabBOM Item`
-					where parent=bom.name and ifnull(bom_no, '')!='')"""
-		)
-
-	while count < len(bom_list):
-		for child_bom in _get_parent(bom_list[count]):
-			if child_bom not in bom_list:
-				bom_list.append(child_bom)
-		count += 1
-
-	return bom_list
-
-
 def add_additional_cost(stock_entry, work_order):
 	# Add non stock items cost in the additional cost
 	stock_entry.additional_costs = []
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index f235e44..182a20c 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -11,7 +11,9 @@
 
 from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
 from erpnext.manufacturing.doctype.bom.bom import BOMRecursionError, item_query, make_variant_bom
-from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
+from erpnext.manufacturing.doctype.bom_update_log.test_bom_update_log import (
+	update_cost_in_all_boms_in_test,
+)
 from erpnext.stock.doctype.item.test_item import make_item
 from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
 	create_stock_reconciliation,
@@ -69,26 +71,31 @@
 
 	def test_update_bom_cost_in_all_boms(self):
 		# get current rate for '_Test Item 2'
-		rm_rate = frappe.db.sql(
-			"""select rate from `tabBOM Item`
-			where parent='BOM-_Test Item Home Desktop Manufactured-001'
-			and item_code='_Test Item 2' and docstatus=1 and parenttype='BOM'"""
+		bom_rates = frappe.db.get_values(
+			"BOM Item",
+			{
+				"parent": "BOM-_Test Item Home Desktop Manufactured-001",
+				"item_code": "_Test Item 2",
+				"docstatus": 1,
+			},
+			fieldname=["rate", "base_rate"],
+			as_dict=True,
 		)
-		rm_rate = rm_rate[0][0] if rm_rate else 0
+		rm_base_rate = bom_rates[0].get("base_rate") if bom_rates else 0
 
 		# Reset item valuation rate
-		reset_item_valuation_rate(item_code="_Test Item 2", qty=200, rate=rm_rate + 10)
+		reset_item_valuation_rate(item_code="_Test Item 2", qty=200, rate=rm_base_rate + 10)
 
 		# update cost of all BOMs based on latest valuation rate
-		update_cost()
+		update_cost_in_all_boms_in_test()
 
 		# check if new valuation rate updated in all BOMs
 		for d in frappe.db.sql(
-			"""select rate from `tabBOM Item`
+			"""select base_rate from `tabBOM Item`
 			where item_code='_Test Item 2' and docstatus=1 and parenttype='BOM'""",
 			as_dict=1,
 		):
-			self.assertEqual(d.rate, rm_rate + 10)
+			self.assertEqual(d.base_rate, rm_base_rate + 10)
 
 	def test_bom_cost(self):
 		bom = frappe.copy_doc(test_records[2])
diff --git a/erpnext/manufacturing/doctype/bom/test_records.json b/erpnext/manufacturing/doctype/bom/test_records.json
index 25730f9..507d319 100644
--- a/erpnext/manufacturing/doctype/bom/test_records.json
+++ b/erpnext/manufacturing/doctype/bom/test_records.json
@@ -32,6 +32,7 @@
   "is_active": 1,
   "is_default": 1,
   "item": "_Test Item Home Desktop Manufactured",
+  "company": "_Test Company",
   "quantity": 1.0
  },
  {
diff --git a/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json b/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json
index f01d856..9b1db63 100644
--- a/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json
+++ b/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json
@@ -169,13 +169,15 @@
  "index_web_pages_for_search": 1,
  "istable": 1,
  "links": [],
- "modified": "2020-10-08 16:21:29.386212",
+ "modified": "2022-05-27 13:42:23.305455",
  "modified_by": "Administrator",
  "module": "Manufacturing",
  "name": "BOM Explosion Item",
+ "naming_rule": "Random",
  "owner": "Administrator",
  "permissions": [],
  "sort_field": "modified",
  "sort_order": "DESC",
+ "states": [],
  "track_changes": 1
 }
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/data_migration_mapping/__init__.py b/erpnext/manufacturing/doctype/bom_update_batch/__init__.py
similarity index 100%
rename from erpnext/erpnext_integrations/data_migration_mapping/__init__.py
rename to erpnext/manufacturing/doctype/bom_update_batch/__init__.py
diff --git a/erpnext/manufacturing/doctype/bom_update_batch/bom_update_batch.json b/erpnext/manufacturing/doctype/bom_update_batch/bom_update_batch.json
new file mode 100644
index 0000000..83b54d3
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_update_batch/bom_update_batch.json
@@ -0,0 +1,55 @@
+{
+ "actions": [],
+ "autoname": "autoincrement",
+ "creation": "2022-05-31 17:34:39.825537",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+  "level",
+  "batch_no",
+  "boms_updated",
+  "status"
+ ],
+ "fields": [
+  {
+   "fieldname": "level",
+   "fieldtype": "Int",
+   "in_list_view": 1,
+   "label": "Level"
+  },
+  {
+   "fieldname": "batch_no",
+   "fieldtype": "Int",
+   "in_list_view": 1,
+   "label": "Batch No."
+  },
+  {
+   "fieldname": "boms_updated",
+   "fieldtype": "Long Text",
+   "hidden": 1,
+   "in_list_view": 1,
+   "label": "BOMs Updated"
+  },
+  {
+   "fieldname": "status",
+   "fieldtype": "Select",
+   "in_list_view": 1,
+   "label": "Status",
+   "options": "Pending\nCompleted",
+   "read_only": 1
+  }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2022-06-06 14:50:35.161062",
+ "modified_by": "Administrator",
+ "module": "Manufacturing",
+ "name": "BOM Update Batch",
+ "naming_rule": "Autoincrement",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/bom_update_batch/bom_update_batch.py b/erpnext/manufacturing/doctype/bom_update_batch/bom_update_batch.py
new file mode 100644
index 0000000..f952e43
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_update_batch/bom_update_batch.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class BOMUpdateBatch(Document):
+	pass
diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json
index 98c1acb..c32e383 100644
--- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json
+++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.json
@@ -13,6 +13,10 @@
   "update_type",
   "status",
   "error_log",
+  "progress_section",
+  "current_level",
+  "processed_boms",
+  "bom_batches",
   "amended_from"
  ],
  "fields": [
@@ -63,13 +67,36 @@
    "fieldtype": "Link",
    "label": "Error Log",
    "options": "Error Log"
+  },
+  {
+   "collapsible": 1,
+   "depends_on": "eval: doc.update_type == \"Update Cost\"",
+   "fieldname": "progress_section",
+   "fieldtype": "Section Break",
+   "label": "Progress"
+  },
+  {
+   "fieldname": "processed_boms",
+   "fieldtype": "Long Text",
+   "hidden": 1,
+   "label": "Processed BOMs"
+  },
+  {
+   "fieldname": "bom_batches",
+   "fieldtype": "Table",
+   "options": "BOM Update Batch"
+  },
+  {
+   "fieldname": "current_level",
+   "fieldtype": "Int",
+   "label": "Current Level"
   }
  ],
  "in_create": 1,
  "index_web_pages_for_search": 1,
  "is_submittable": 1,
  "links": [],
- "modified": "2022-03-31 12:51:44.885102",
+ "modified": "2022-06-06 15:15:23.883251",
  "modified_by": "Administrator",
  "module": "Manufacturing",
  "name": "BOM Update Log",
diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py
index c0770fa..9c9c240 100644
--- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py
+++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py
@@ -1,13 +1,20 @@
 # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
 # For license information, please see license.txt
-from typing import Dict, List, Literal, Optional
+import json
+from typing import Any, Dict, List, Optional, Tuple, Union
 
 import frappe
 from frappe import _
 from frappe.model.document import Document
-from frappe.utils import cstr, flt
+from frappe.utils import cint, cstr
 
-from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
+from erpnext.manufacturing.doctype.bom_update_log.bom_updation_utils import (
+	get_leaf_boms,
+	get_next_higher_level_boms,
+	handle_exception,
+	replace_bom,
+	set_values_in_log,
+)
 
 
 class BOMMissingError(frappe.ValidationError):
@@ -20,6 +27,8 @@
 			self.validate_boms_are_specified()
 			self.validate_same_bom()
 			self.validate_bom_items()
+		else:
+			self.validate_bom_cost_update_in_progress()
 
 		self.status = "Queued"
 
@@ -42,123 +51,184 @@
 		if current_bom_item != new_bom_item:
 			frappe.throw(_("The selected BOMs are not for the same item"))
 
-	def on_submit(self):
-		if frappe.flags.in_test:
-			return
+	def validate_bom_cost_update_in_progress(self):
+		"If another Cost Updation Log is still in progress, dont make new ones."
 
+		wip_log = frappe.get_all(
+			"BOM Update Log",
+			{"update_type": "Update Cost", "status": ["in", ["Queued", "In Progress"]]},
+			limit_page_length=1,
+		)
+		if wip_log:
+			log_link = frappe.utils.get_link_to_form("BOM Update Log", wip_log[0].name)
+			frappe.throw(
+				_("BOM Updation already in progress. Please wait until {0} is complete.").format(log_link),
+				title=_("Note"),
+			)
+
+	def on_submit(self):
 		if self.update_type == "Replace BOM":
 			boms = {"current_bom": self.current_bom, "new_bom": self.new_bom}
 			frappe.enqueue(
-				method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.run_bom_job",
+				method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.run_replace_bom_job",
 				doc=self,
 				boms=boms,
 				timeout=40000,
+				now=frappe.flags.in_test,
 			)
 		else:
-			frappe.enqueue(
-				method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.run_bom_job",
-				doc=self,
-				update_type="Update Cost",
-				timeout=40000,
-			)
+			process_boms_cost_level_wise(self)
 
 
-def replace_bom(boms: Dict) -> None:
-	"""Replace current BOM with new BOM in parent BOMs."""
-	current_bom = boms.get("current_bom")
-	new_bom = boms.get("new_bom")
-
-	unit_cost = get_new_bom_unit_cost(new_bom)
-	update_new_bom_in_bom_items(unit_cost, current_bom, new_bom)
-
-	frappe.cache().delete_key("bom_children")
-	parent_boms = get_parent_boms(new_bom)
-
-	for bom in parent_boms:
-		bom_obj = frappe.get_doc("BOM", bom)
-		# this is only used for versioning and we do not want
-		# to make separate db calls by using load_doc_before_save
-		# which proves to be expensive while doing bulk replace
-		bom_obj._doc_before_save = bom_obj
-		bom_obj.update_exploded_items()
-		bom_obj.calculate_cost()
-		bom_obj.update_parent_cost()
-		bom_obj.db_update()
-		if bom_obj.meta.get("track_changes") and not bom_obj.flags.ignore_version:
-			bom_obj.save_version()
-
-
-def update_new_bom_in_bom_items(unit_cost: float, current_bom: str, new_bom: str) -> None:
-	bom_item = frappe.qb.DocType("BOM Item")
-	(
-		frappe.qb.update(bom_item)
-		.set(bom_item.bom_no, new_bom)
-		.set(bom_item.rate, unit_cost)
-		.set(bom_item.amount, (bom_item.stock_qty * unit_cost))
-		.where(
-			(bom_item.bom_no == current_bom) & (bom_item.docstatus < 2) & (bom_item.parenttype == "BOM")
-		)
-	).run()
-
-
-def get_parent_boms(new_bom: str, bom_list: Optional[List] = None) -> List:
-	bom_list = bom_list or []
-	bom_item = frappe.qb.DocType("BOM Item")
-
-	parents = (
-		frappe.qb.from_(bom_item)
-		.select(bom_item.parent)
-		.where((bom_item.bom_no == new_bom) & (bom_item.docstatus < 2) & (bom_item.parenttype == "BOM"))
-		.run(as_dict=True)
-	)
-
-	for d in parents:
-		if new_bom == d.parent:
-			frappe.throw(_("BOM recursion: {0} cannot be child of {1}").format(new_bom, d.parent))
-
-		bom_list.append(d.parent)
-		get_parent_boms(d.parent, bom_list)
-
-	return list(set(bom_list))
-
-
-def get_new_bom_unit_cost(new_bom: str) -> float:
-	bom = frappe.qb.DocType("BOM")
-	new_bom_unitcost = (
-		frappe.qb.from_(bom).select(bom.total_cost / bom.quantity).where(bom.name == new_bom).run()
-	)
-
-	return flt(new_bom_unitcost[0][0])
-
-
-def run_bom_job(
+def run_replace_bom_job(
 	doc: "BOMUpdateLog",
 	boms: Optional[Dict[str, str]] = None,
-	update_type: Literal["Replace BOM", "Update Cost"] = "Replace BOM",
 ) -> None:
 	try:
 		doc.db_set("status", "In Progress")
+
 		if not frappe.flags.in_test:
 			frappe.db.commit()
 
 		frappe.db.auto_commit_on_many_writes = 1
-
 		boms = frappe._dict(boms or {})
-
-		if update_type == "Replace BOM":
-			replace_bom(boms)
-		else:
-			update_cost()
+		replace_bom(boms, doc.name)
 
 		doc.db_set("status", "Completed")
-
 	except Exception:
-		frappe.db.rollback()
-		error_log = doc.log_error("BOM Update Tool Error")
-
-		doc.db_set("status", "Failed")
-		doc.db_set("error_log", error_log.name)
-
+		handle_exception(doc)
 	finally:
 		frappe.db.auto_commit_on_many_writes = 0
-		frappe.db.commit()  # nosemgrep
+
+		if not frappe.flags.in_test:
+			frappe.db.commit()  # nosemgrep
+
+
+def process_boms_cost_level_wise(
+	update_doc: "BOMUpdateLog", parent_boms: List[str] = None
+) -> Union[None, Tuple]:
+	"Queue jobs at the start of new BOM Level in 'Update Cost' Jobs."
+
+	current_boms = {}
+	values = {}
+
+	if update_doc.status == "Queued":
+		# First level yet to process. On Submit.
+		current_level = 0
+		current_boms = get_leaf_boms()
+		values = {
+			"processed_boms": json.dumps({}),
+			"status": "In Progress",
+			"current_level": current_level,
+		}
+	else:
+		# Resume next level. via Cron Job.
+		if not parent_boms:
+			return
+
+		current_level = cint(update_doc.current_level) + 1
+
+		# Process the next level BOMs. Stage parents as current BOMs.
+		current_boms = parent_boms.copy()
+		values = {"current_level": current_level}
+
+	set_values_in_log(update_doc.name, values, commit=True)
+	queue_bom_cost_jobs(current_boms, update_doc, current_level)
+
+
+def queue_bom_cost_jobs(
+	current_boms_list: List[str], update_doc: "BOMUpdateLog", current_level: int
+) -> None:
+	"Queue batches of 20k BOMs of the same level to process parallelly"
+	batch_no = 0
+
+	while current_boms_list:
+		batch_no += 1
+		batch_size = 20_000
+		boms_to_process = current_boms_list[:batch_size]  # slice out batch of 20k BOMs
+
+		# update list to exclude 20K (queued) BOMs
+		current_boms_list = current_boms_list[batch_size:] if len(current_boms_list) > batch_size else []
+
+		batch_row = update_doc.append(
+			"bom_batches", {"level": current_level, "batch_no": batch_no, "status": "Pending"}
+		)
+		batch_row.db_insert()
+
+		frappe.enqueue(
+			method="erpnext.manufacturing.doctype.bom_update_log.bom_updation_utils.update_cost_in_level",
+			doc=update_doc,
+			bom_list=boms_to_process,
+			batch_name=batch_row.name,
+			queue="long",
+			now=frappe.flags.in_test,
+		)
+
+
+def resume_bom_cost_update_jobs():
+	"""
+	1. Checks for In Progress BOM Update Log.
+	2. Checks if this job has completed the _current level_.
+	3. If current level is complete, get parent BOMs and start next level.
+	4. If no parents, mark as Complete.
+	5. If current level is WIP, skip the Log.
+
+	Called every 5 minutes via Cron job.
+	"""
+
+	in_progress_logs = frappe.db.get_all(
+		"BOM Update Log",
+		{"update_type": "Update Cost", "status": "In Progress"},
+		["name", "processed_boms", "current_level"],
+	)
+	if not in_progress_logs:
+		return
+
+	for log in in_progress_logs:
+		# check if all log batches of current level are processed
+		bom_batches = frappe.db.get_all(
+			"BOM Update Batch",
+			{"parent": log.name, "level": log.current_level},
+			["name", "boms_updated", "status"],
+		)
+		incomplete_level = any(row.get("status") == "Pending" for row in bom_batches)
+		if not bom_batches or incomplete_level:
+			continue
+
+		# Prep parent BOMs & updated processed BOMs for next level
+		current_boms, processed_boms = get_processed_current_boms(log, bom_batches)
+		parent_boms = get_next_higher_level_boms(child_boms=current_boms, processed_boms=processed_boms)
+
+		# Unset processed BOMs if log is complete, it is used for next level BOMs
+		set_values_in_log(
+			log.name,
+			values={
+				"processed_boms": json.dumps([] if not parent_boms else processed_boms),
+				"status": "Completed" if not parent_boms else "In Progress",
+			},
+			commit=True,
+		)
+
+		if parent_boms:  # there is a next level to process
+			process_boms_cost_level_wise(
+				update_doc=frappe.get_doc("BOM Update Log", log.name), parent_boms=parent_boms
+			)
+
+
+def get_processed_current_boms(
+	log: Dict[str, Any], bom_batches: Dict[str, Any]
+) -> Tuple[List[str], Dict[str, Any]]:
+	"""
+	Aggregate all BOMs from BOM Update Batch rows into 'processed_boms' field
+	and into current boms list.
+	"""
+	processed_boms = json.loads(log.processed_boms) if log.processed_boms else {}
+	current_boms = []
+
+	for row in bom_batches:
+		boms_updated = json.loads(row.boms_updated)
+		current_boms.extend(boms_updated)
+		boms_updated_dict = {bom: True for bom in boms_updated}
+		processed_boms.update(boms_updated_dict)
+
+	return current_boms, processed_boms
diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py b/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py
new file mode 100644
index 0000000..af115e3
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py
@@ -0,0 +1,225 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import copy
+import json
+from collections import defaultdict
+from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
+
+if TYPE_CHECKING:
+	from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import BOMUpdateLog
+
+import frappe
+from frappe import _
+
+
+def replace_bom(boms: Dict, log_name: str) -> None:
+	"Replace current BOM with new BOM in parent BOMs."
+
+	current_bom = boms.get("current_bom")
+	new_bom = boms.get("new_bom")
+
+	unit_cost = get_bom_unit_cost(new_bom)
+	update_new_bom_in_bom_items(unit_cost, current_bom, new_bom)
+
+	frappe.cache().delete_key("bom_children")
+	parent_boms = get_ancestor_boms(new_bom)
+
+	for bom in parent_boms:
+		bom_obj = frappe.get_doc("BOM", bom)
+		# this is only used for versioning and we do not want
+		# to make separate db calls by using load_doc_before_save
+		# which proves to be expensive while doing bulk replace
+		bom_obj._doc_before_save = copy.deepcopy(bom_obj)
+		bom_obj.update_exploded_items()
+		bom_obj.calculate_cost()
+		bom_obj.update_parent_cost()
+		bom_obj.db_update()
+		bom_obj.flags.updater_reference = {
+			"doctype": "BOM Update Log",
+			"docname": log_name,
+			"label": _("via BOM Update Tool"),
+		}
+		bom_obj.save_version()
+
+
+def update_cost_in_level(
+	doc: "BOMUpdateLog", bom_list: List[str], batch_name: Union[int, str]
+) -> None:
+	"Updates Cost for BOMs within a given level. Runs via background jobs."
+
+	try:
+		status = frappe.db.get_value("BOM Update Log", doc.name, "status")
+		if status == "Failed":
+			return
+
+		update_cost_in_boms(bom_list=bom_list)  # main updation logic
+
+		bom_batch = frappe.qb.DocType("BOM Update Batch")
+		(
+			frappe.qb.update(bom_batch)
+			.set(bom_batch.boms_updated, json.dumps(bom_list))
+			.set(bom_batch.status, "Completed")
+			.where(bom_batch.name == batch_name)
+		).run()
+	except Exception:
+		handle_exception(doc)
+	finally:
+		if not frappe.flags.in_test:
+			frappe.db.commit()  # nosemgrep
+
+
+def get_ancestor_boms(new_bom: str, bom_list: Optional[List] = None) -> List:
+	"Recursively get all ancestors of BOM."
+
+	bom_list = bom_list or []
+	bom_item = frappe.qb.DocType("BOM Item")
+
+	parents = (
+		frappe.qb.from_(bom_item)
+		.select(bom_item.parent)
+		.where((bom_item.bom_no == new_bom) & (bom_item.docstatus < 2) & (bom_item.parenttype == "BOM"))
+		.run(as_dict=True)
+	)
+
+	for d in parents:
+		if new_bom == d.parent:
+			frappe.throw(_("BOM recursion: {0} cannot be child of {1}").format(new_bom, d.parent))
+
+		bom_list.append(d.parent)
+		get_ancestor_boms(d.parent, bom_list)
+
+	return list(set(bom_list))
+
+
+def update_new_bom_in_bom_items(unit_cost: float, current_bom: str, new_bom: str) -> None:
+	bom_item = frappe.qb.DocType("BOM Item")
+	(
+		frappe.qb.update(bom_item)
+		.set(bom_item.bom_no, new_bom)
+		.set(bom_item.rate, unit_cost)
+		.set(bom_item.amount, (bom_item.stock_qty * unit_cost))
+		.where(
+			(bom_item.bom_no == current_bom) & (bom_item.docstatus < 2) & (bom_item.parenttype == "BOM")
+		)
+	).run()
+
+
+def get_bom_unit_cost(bom_name: str) -> float:
+	bom = frappe.qb.DocType("BOM")
+	new_bom_unitcost = (
+		frappe.qb.from_(bom).select(bom.total_cost / bom.quantity).where(bom.name == bom_name).run()
+	)
+
+	return frappe.utils.flt(new_bom_unitcost[0][0])
+
+
+def update_cost_in_boms(bom_list: List[str]) -> None:
+	"Updates cost in given BOMs. Returns current and total updated BOMs."
+
+	for index, bom in enumerate(bom_list):
+		bom_doc = frappe.get_doc("BOM", bom, for_update=True)
+		bom_doc.calculate_cost(save_updates=True, update_hour_rate=True)
+		bom_doc.db_update()
+
+		if (index % 50 == 0) and not frappe.flags.in_test:
+			frappe.db.commit()  # nosemgrep
+
+
+def get_next_higher_level_boms(
+	child_boms: List[str], processed_boms: Dict[str, bool]
+) -> List[str]:
+	"Generate immediate higher level dependants with no unresolved dependencies (children)."
+
+	def _all_children_are_processed(parent_bom):
+		child_boms = dependency_map.get(parent_bom)
+		return all(processed_boms.get(bom) for bom in child_boms)
+
+	dependants_map, dependency_map = _generate_dependence_map()
+
+	dependants = []
+	for bom in child_boms:
+		# generate list of immediate dependants
+		parents = dependants_map.get(bom) or []
+		dependants.extend(parents)
+
+	dependants = set(dependants)  # remove duplicates
+	resolved_dependants = set()
+
+	# consider only if children are all resolved
+	for parent_bom in dependants:
+		if _all_children_are_processed(parent_bom):
+			resolved_dependants.add(parent_bom)
+
+	return list(resolved_dependants)
+
+
+def get_leaf_boms() -> List[str]:
+	"Get BOMs that have no dependencies."
+
+	return frappe.db.sql_list(
+		"""select name from `tabBOM` bom
+		where docstatus=1 and is_active=1
+			and not exists(select bom_no from `tabBOM Item`
+				where parent=bom.name and ifnull(bom_no, '')!='')"""
+	)
+
+
+def _generate_dependence_map() -> defaultdict:
+	"""
+	Generate maps such as: { BOM-1: [Dependant-BOM-1, Dependant-BOM-2, ..] }.
+	Here BOM-1 is the leaf/lower level node/dependency.
+	The list contains one level higher nodes/dependants that depend on BOM-1.
+
+	Generate and return the reverse as well.
+	"""
+
+	bom = frappe.qb.DocType("BOM")
+	bom_item = frappe.qb.DocType("BOM Item")
+
+	bom_items = (
+		frappe.qb.from_(bom_item)
+		.join(bom)
+		.on(bom_item.parent == bom.name)
+		.select(bom_item.bom_no, bom_item.parent)
+		.where(
+			(bom_item.bom_no.isnotnull())
+			& (bom_item.bom_no != "")
+			& (bom.docstatus == 1)
+			& (bom.is_active == 1)
+			& (bom_item.parenttype == "BOM")
+		)
+	).run(as_dict=True)
+
+	child_parent_map = defaultdict(list)
+	parent_child_map = defaultdict(list)
+	for row in bom_items:
+		child_parent_map[row.bom_no].append(row.parent)
+		parent_child_map[row.parent].append(row.bom_no)
+
+	return child_parent_map, parent_child_map
+
+
+def set_values_in_log(log_name: str, values: Dict[str, Any], commit: bool = False) -> None:
+	"Update BOM Update Log record."
+
+	if not values:
+		return
+
+	bom_update_log = frappe.qb.DocType("BOM Update Log")
+	query = frappe.qb.update(bom_update_log).where(bom_update_log.name == log_name)
+
+	for key, value in values.items():
+		query = query.set(key, value)
+	query.run()
+
+	if commit and not frappe.flags.in_test:
+		frappe.db.commit()  # nosemgrep
+
+
+def handle_exception(doc: "BOMUpdateLog") -> None:
+	"Rolls back and fails BOM Update Log."
+
+	frappe.db.rollback()
+	error_log = doc.log_error("BOM Update Tool Error")
+	set_values_in_log(doc.name, {"status": "Failed", "error_log": error_log.name})
diff --git a/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py
index 47efea9..b38fc89 100644
--- a/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py
+++ b/erpnext/manufacturing/doctype/bom_update_log/test_bom_update_log.py
@@ -6,9 +6,12 @@
 
 from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import (
 	BOMMissingError,
-	run_bom_job,
+	resume_bom_cost_update_jobs,
 )
-from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import enqueue_replace_bom
+from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import (
+	enqueue_replace_bom,
+	enqueue_update_cost,
+)
 
 test_records = frappe.get_test_records("BOM")
 
@@ -31,17 +34,12 @@
 	def tearDown(self):
 		frappe.db.rollback()
 
-		if self._testMethodName == "test_bom_update_log_completion":
-			# clear logs and delete BOM created via setUp
-			frappe.db.delete("BOM Update Log")
-			self.new_bom_doc.cancel()
-			self.new_bom_doc.delete()
-
-			# explicitly commit and restore to original state
-			frappe.db.commit()  # nosemgrep
-
 	def test_bom_update_log_validate(self):
-		"Test if BOM presence is validated."
+		"""
+		1) Test if BOM presence is validated.
+		2) Test if same BOMs are validated.
+		3) Test of non-existent BOM is validated.
+		"""
 
 		with self.assertRaises(BOMMissingError):
 			enqueue_replace_bom(boms={})
@@ -52,45 +50,22 @@
 		with self.assertRaises(frappe.ValidationError):
 			enqueue_replace_bom(boms=frappe._dict(current_bom=self.boms.new_bom, new_bom="Dummy BOM"))
 
-	def test_bom_update_log_queueing(self):
-		"Test if BOM Update Log is created and queued."
-
-		log = enqueue_replace_bom(
-			boms=self.boms,
-		)
-
-		self.assertEqual(log.docstatus, 1)
-		self.assertEqual(log.status, "Queued")
-
 	def test_bom_update_log_completion(self):
 		"Test if BOM Update Log handles job completion correctly."
 
-		log = enqueue_replace_bom(
-			boms=self.boms,
-		)
-
-		# Explicitly commits log, new bom (setUp) and replacement impact.
-		# Is run via background jobs IRL
-		run_bom_job(
-			doc=log,
-			boms=self.boms,
-			update_type="Replace BOM",
-		)
+		log = enqueue_replace_bom(boms=self.boms)
 		log.reload()
-
 		self.assertEqual(log.status, "Completed")
 
-		# teardown (undo replace impact) due to commit
-		boms = frappe._dict(
-			current_bom=self.boms.new_bom,
-			new_bom=self.boms.current_bom,
-		)
-		log2 = enqueue_replace_bom(
-			boms=self.boms,
-		)
-		run_bom_job(  # Explicitly commits
-			doc=log2,
-			boms=boms,
-			update_type="Replace BOM",
-		)
-		self.assertEqual(log2.status, "Completed")
+
+def update_cost_in_all_boms_in_test():
+	"""
+	Utility to run 'Update Cost' job in tests without Cron job until fully complete.
+	"""
+	log = enqueue_update_cost()  # create BOM Update Log
+
+	while log.status != "Completed":
+		resume_bom_cost_update_jobs()  # run cron job until complete
+		log.reload()
+
+	return log
diff --git a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py
index b0e7da1..d16fcd0 100644
--- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py
+++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py
@@ -10,8 +10,6 @@
 import frappe
 from frappe.model.document import Document
 
-from erpnext.manufacturing.doctype.bom.bom import get_boms_in_bottom_up_order
-
 
 class BOMUpdateTool(Document):
 	pass
@@ -40,14 +38,13 @@
 def auto_update_latest_price_in_all_boms() -> None:
 	"""Called via hooks.py."""
 	if frappe.db.get_single_value("Manufacturing Settings", "update_bom_costs_automatically"):
-		update_cost()
-
-
-def update_cost() -> None:
-	"""Updates Cost for all BOMs from bottom to top."""
-	bom_list = get_boms_in_bottom_up_order()
-	for bom in bom_list:
-		frappe.get_doc("BOM", bom).update_cost(update_parent=False, from_child_bom=True)
+		wip_log = frappe.get_all(
+			"BOM Update Log",
+			{"update_type": "Update Cost", "status": ["in", ["Queued", "In Progress"]]},
+			limit_page_length=1,
+		)
+		if not wip_log:
+			create_bom_update_log(update_type="Update Cost")
 
 
 def create_bom_update_log(
diff --git a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py
index fae72a0..5dd557f 100644
--- a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py
+++ b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py
@@ -1,11 +1,13 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
 # License: GNU General Public License v3. See license.txt
 
 import frappe
 from frappe.tests.utils import FrappeTestCase
 
-from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import replace_bom
-from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
+from erpnext.manufacturing.doctype.bom_update_log.test_bom_update_log import (
+	update_cost_in_all_boms_in_test,
+)
+from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import enqueue_replace_bom
 from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
 from erpnext.stock.doctype.item.test_item import create_item
 
@@ -15,6 +17,9 @@
 class TestBOMUpdateTool(FrappeTestCase):
 	"Test major functions run via BOM Update Tool."
 
+	def tearDown(self):
+		frappe.db.rollback()
+
 	def test_replace_bom(self):
 		current_bom = "BOM-_Test Item Home Desktop Manufactured-001"
 
@@ -23,15 +28,10 @@
 		bom_doc.insert()
 
 		boms = frappe._dict(current_bom=current_bom, new_bom=bom_doc.name)
-		replace_bom(boms)
+		enqueue_replace_bom(boms=boms)
 
-		self.assertFalse(frappe.db.sql("select name from `tabBOM Item` where bom_no=%s", current_bom))
-		self.assertTrue(frappe.db.sql("select name from `tabBOM Item` where bom_no=%s", bom_doc.name))
-
-		# reverse, as it affects other testcases
-		boms.current_bom = bom_doc.name
-		boms.new_bom = current_bom
-		replace_bom(boms)
+		self.assertFalse(frappe.db.exists("BOM Item", {"bom_no": current_bom, "docstatus": 1}))
+		self.assertTrue(frappe.db.exists("BOM Item", {"bom_no": bom_doc.name, "docstatus": 1}))
 
 	def test_bom_cost(self):
 		for item in ["BOM Cost Test Item 1", "BOM Cost Test Item 2", "BOM Cost Test Item 3"]:
@@ -52,13 +52,13 @@
 		self.assertEqual(doc.total_cost, 200)
 
 		frappe.db.set_value("Item", "BOM Cost Test Item 2", "valuation_rate", 200)
-		update_cost()
+		update_cost_in_all_boms_in_test()
 
 		doc.load_from_db()
 		self.assertEqual(doc.total_cost, 300)
 
 		frappe.db.set_value("Item", "BOM Cost Test Item 2", "valuation_rate", 100)
-		update_cost()
+		update_cost_in_all_boms_in_test()
 
 		doc.load_from_db()
 		self.assertEqual(doc.total_cost, 200)
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index 0a9fd8a..0199a5c 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -621,7 +621,7 @@
 		self.set_status(update_status)
 
 	def set_status(self, update_status=False):
-		if self.status == "On Hold":
+		if self.status == "On Hold" and self.docstatus == 0:
 			return
 
 		self.status = {0: "Open", 1: "Submitted", 2: "Cancelled"}[self.docstatus or 0]
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index 891a497..e88049d 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -798,7 +798,6 @@
 
 	for item in args.raw_materials:
 		item_doc = frappe.get_doc("Item", item)
-
 		bom.append(
 			"items",
 			{
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index 2aba482..27e7e24 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -417,7 +417,7 @@
 					"doctype": "Item Price",
 					"item_code": "_Test FG Non Stock Item",
 					"price_list_rate": 1000,
-					"price_list": "Standard Buying",
+					"price_list": "_Test Price List India",
 				}
 			).insert(ignore_permissions=True)
 
@@ -426,8 +426,17 @@
 			item_code="_Test FG Item", target="_Test Warehouse - _TC", qty=1, basic_rate=100
 		)
 
-		if not frappe.db.get_value("BOM", {"item": fg_item}):
-			make_bom(item=fg_item, rate=1000, raw_materials=["_Test FG Item", "_Test FG Non Stock Item"])
+		if not frappe.db.get_value("BOM", {"item": fg_item, "docstatus": 1}):
+			bom = make_bom(
+				item=fg_item,
+				rate=1000,
+				raw_materials=["_Test FG Item", "_Test FG Non Stock Item"],
+				do_not_save=True,
+			)
+			bom.rm_cost_as_per = "Price List"  # non stock item won't have valuation rate
+			bom.buying_price_list = "_Test Price List India"
+			bom.currency = "INR"
+			bom.save()
 
 		wo = make_wo_order_test_record(production_item=fg_item)
 
diff --git a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json
index 05ca2a8..9829a96 100644
--- a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json
+++ b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json
@@ -402,14 +402,15 @@
    "type": "Link"
   }
  ],
- "modified": "2022-01-13 17:40:09.474747",
+ "modified": "2022-05-31 22:08:19.408223",
  "modified_by": "Administrator",
  "module": "Manufacturing",
  "name": "Manufacturing",
  "owner": "Administrator",
  "parent_page": "",
  "public": 1,
- "restrict_to_domain": "Manufacturing",
+ "quick_lists": [],
+ "restrict_to_domain": "",
  "roles": [],
  "sequence_id": 17.0,
  "shortcuts": [
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 8c0ebe7..5a98463 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -231,7 +231,6 @@
 erpnext.patches.v13_0.update_reason_for_resignation_in_employee
 execute:frappe.delete_doc("Report", "Quoted Item Comparison")
 erpnext.patches.v13_0.update_member_email_address
-erpnext.patches.v13_0.create_leave_policy_assignment_based_on_employee_current_leave_policy
 erpnext.patches.v13_0.update_pos_closing_entry_in_merge_log
 erpnext.patches.v13_0.add_po_to_global_search
 erpnext.patches.v13_0.update_returned_qty_in_pr_dn
@@ -372,3 +371,6 @@
 erpnext.patches.v14_0.delete_employee_transfer_property_doctype
 erpnext.patches.v13_0.create_accounting_dimensions_in_orders
 erpnext.patches.v13_0.set_per_billed_in_return_delivery_note
+execute:frappe.delete_doc("DocType", "Naming Series")
+erpnext.patches.v13_0.set_payroll_entry_status
+erpnext.patches.v13_0.job_card_status_on_hold
diff --git a/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py b/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py
deleted file mode 100644
index 59b17ee..0000000
--- a/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py
+++ /dev/null
@@ -1,94 +0,0 @@
-# Copyright (c) 2019, Frappe and Contributors
-# License: GNU General Public License v3. See license.txt
-
-
-import frappe
-
-
-def execute():
-	frappe.reload_doc("hr", "doctype", "leave_policy_assignment")
-	frappe.reload_doc("hr", "doctype", "employee_grade")
-	employee_with_assignment = []
-	leave_policy = []
-
-	if "leave_policy" in frappe.db.get_table_columns("Employee"):
-		employees_with_leave_policy = frappe.db.sql(
-			"SELECT name, leave_policy FROM `tabEmployee` WHERE leave_policy IS NOT NULL and leave_policy != ''",
-			as_dict=1,
-		)
-
-		for employee in employees_with_leave_policy:
-			alloc = frappe.db.exists(
-				"Leave Allocation",
-				{"employee": employee.name, "leave_policy": employee.leave_policy, "docstatus": 1},
-			)
-			if not alloc:
-				create_assignment(employee.name, employee.leave_policy)
-
-			employee_with_assignment.append(employee.name)
-			leave_policy.append(employee.leave_policy)
-
-	if "default_leave_policy" in frappe.db.get_table_columns("Employee Grade"):
-		employee_grade_with_leave_policy = frappe.db.sql(
-			"SELECT name, default_leave_policy FROM `tabEmployee Grade` WHERE default_leave_policy IS NOT NULL and default_leave_policy!=''",
-			as_dict=1,
-		)
-
-		# for whole employee Grade
-		for grade in employee_grade_with_leave_policy:
-			employees = get_employee_with_grade(grade.name)
-			for employee in employees:
-
-				if employee not in employee_with_assignment:  # Will ensure no duplicate
-					alloc = frappe.db.exists(
-						"Leave Allocation",
-						{"employee": employee.name, "leave_policy": grade.default_leave_policy, "docstatus": 1},
-					)
-					if not alloc:
-						create_assignment(employee.name, grade.default_leave_policy)
-					leave_policy.append(grade.default_leave_policy)
-
-	# for old Leave allocation and leave policy from allocation, which may got updated in employee grade.
-	leave_allocations = frappe.db.sql(
-		"SELECT leave_policy, leave_period, employee FROM `tabLeave Allocation` WHERE leave_policy IS NOT NULL and leave_policy != '' and docstatus = 1 ",
-		as_dict=1,
-	)
-
-	for allocation in leave_allocations:
-		if allocation.leave_policy not in leave_policy:
-			create_assignment(
-				allocation.employee,
-				allocation.leave_policy,
-				leave_period=allocation.leave_period,
-				allocation_exists=True,
-			)
-
-
-def create_assignment(employee, leave_policy, leave_period=None, allocation_exists=False):
-	if frappe.db.get_value("Leave Policy", leave_policy, "docstatus") == 2:
-		return
-
-	filters = {"employee": employee, "leave_policy": leave_policy}
-	if leave_period:
-		filters["leave_period"] = leave_period
-
-	if not frappe.db.exists("Leave Policy Assignment", filters):
-		lpa = frappe.new_doc("Leave Policy Assignment")
-		lpa.employee = employee
-		lpa.leave_policy = leave_policy
-
-		lpa.flags.ignore_mandatory = True
-		if allocation_exists:
-			lpa.assignment_based_on = "Leave Period"
-			lpa.leave_period = leave_period
-			lpa.leaves_allocated = 1
-
-		lpa.save()
-		if allocation_exists:
-			lpa.submit()
-			# Updating old Leave Allocation
-			frappe.db.sql("Update `tabLeave Allocation` set leave_policy_assignment = %s", lpa.name)
-
-
-def get_employee_with_grade(grade):
-	return frappe.get_list("Employee", filters={"grade": grade})
diff --git a/erpnext/patches/v13_0/item_naming_series_not_mandatory.py b/erpnext/patches/v13_0/item_naming_series_not_mandatory.py
index 33fb8f9..0235a62 100644
--- a/erpnext/patches/v13_0/item_naming_series_not_mandatory.py
+++ b/erpnext/patches/v13_0/item_naming_series_not_mandatory.py
@@ -1,6 +1,6 @@
 import frappe
 
-from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series
+from erpnext.utilities.naming import set_by_naming_series
 
 
 def execute():
diff --git a/erpnext/patches/v13_0/job_card_status_on_hold.py b/erpnext/patches/v13_0/job_card_status_on_hold.py
new file mode 100644
index 0000000..8c67c3c
--- /dev/null
+++ b/erpnext/patches/v13_0/job_card_status_on_hold.py
@@ -0,0 +1,19 @@
+import frappe
+
+
+def execute():
+	job_cards = frappe.get_all(
+		"Job Card",
+		{"status": "On Hold", "docstatus": ("!=", 0)},
+		pluck="name",
+	)
+
+	for idx, job_card in enumerate(job_cards):
+		try:
+			doc = frappe.get_doc("Job Card", job_card)
+			doc.set_status()
+			doc.db_set("status", doc.status, update_modified=False)
+			if idx % 100 == 0:
+				frappe.db.commit()
+		except Exception:
+			continue
diff --git a/erpnext/patches/v13_0/set_payroll_entry_status.py b/erpnext/patches/v13_0/set_payroll_entry_status.py
new file mode 100644
index 0000000..97adff9
--- /dev/null
+++ b/erpnext/patches/v13_0/set_payroll_entry_status.py
@@ -0,0 +1,16 @@
+import frappe
+from frappe.query_builder import Case
+
+
+def execute():
+	PayrollEntry = frappe.qb.DocType("Payroll Entry")
+
+	(
+		frappe.qb.update(PayrollEntry).set(
+			"status",
+			Case()
+			.when(PayrollEntry.docstatus == 0, "Draft")
+			.when(PayrollEntry.docstatus == 1, "Submitted")
+			.else_("Cancelled"),
+		)
+	).run()
diff --git a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py
index 0acd447..8df1bb6 100644
--- a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py
+++ b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py
@@ -5,7 +5,7 @@
 import frappe
 from frappe import _
 from frappe.model.document import Document
-from frappe.utils import add_days, cint, cstr, date_diff, getdate, rounded
+from frappe.utils import add_days, cstr, date_diff, flt, getdate, rounded
 
 from erpnext.hr.utils import (
 	get_holiday_dates_for_employee,
@@ -27,11 +27,14 @@
 		validate_active_employee(self.employee)
 		self.validate_duplicate_on_payroll_period()
 		if not self.max_benefits:
-			self.max_benefits = get_max_benefits_remaining(self.employee, self.date, self.payroll_period)
+			self.max_benefits = flt(
+				get_max_benefits_remaining(self.employee, self.date, self.payroll_period),
+				self.precision("max_benefits"),
+			)
 		if self.max_benefits and self.max_benefits > 0:
 			self.validate_max_benefit_for_component()
 			self.validate_prev_benefit_claim()
-			if self.remaining_benefit > 0:
+			if self.remaining_benefit and self.remaining_benefit > 0:
 				self.validate_remaining_benefit_amount()
 		else:
 			frappe.throw(
@@ -110,7 +113,7 @@
 			max_benefit_amount = 0
 			for employee_benefit in self.employee_benefits:
 				self.validate_max_benefit(employee_benefit.earning_component)
-				max_benefit_amount += employee_benefit.amount
+				max_benefit_amount += flt(employee_benefit.amount)
 			if max_benefit_amount > self.max_benefits:
 				frappe.throw(
 					_("Maximum benefit amount of employee {0} exceeds {1}").format(
@@ -125,7 +128,8 @@
 		benefit_amount = 0
 		for employee_benefit in self.employee_benefits:
 			if employee_benefit.earning_component == earning_component_name:
-				benefit_amount += employee_benefit.amount
+				benefit_amount += flt(employee_benefit.amount)
+
 		prev_sal_slip_flexi_amount = get_sal_slip_total_benefit_given(
 			self.employee, frappe.get_doc("Payroll Period", self.payroll_period), earning_component_name
 		)
@@ -207,26 +211,47 @@
 def calculate_lwp(employee, start_date, holidays, working_days):
 	lwp = 0
 	holidays = "','".join(holidays)
+
 	for d in range(working_days):
-		dt = add_days(cstr(getdate(start_date)), d)
-		leave = frappe.db.sql(
-			"""
-			select t1.name, t1.half_day
-			from `tabLeave Application` t1, `tabLeave Type` t2
-			where t2.name = t1.leave_type
-			and t2.is_lwp = 1
-			and t1.docstatus = 1
-			and t1.employee = %(employee)s
-			and CASE WHEN t2.include_holiday != 1 THEN %(dt)s not in ('{0}') and %(dt)s between from_date and to_date
-			WHEN t2.include_holiday THEN %(dt)s between from_date and to_date
-			END
-			""".format(
-				holidays
-			),
-			{"employee": employee, "dt": dt},
+		date = add_days(cstr(getdate(start_date)), d)
+
+		LeaveApplication = frappe.qb.DocType("Leave Application")
+		LeaveType = frappe.qb.DocType("Leave Type")
+
+		is_half_day = (
+			frappe.qb.terms.Case()
+			.when(
+				(
+					(LeaveApplication.half_day_date == date)
+					| (LeaveApplication.from_date == LeaveApplication.to_date)
+				),
+				LeaveApplication.half_day,
+			)
+			.else_(0)
+		).as_("is_half_day")
+
+		query = (
+			frappe.qb.from_(LeaveApplication)
+			.inner_join(LeaveType)
+			.on((LeaveType.name == LeaveApplication.leave_type))
+			.select(LeaveApplication.name, is_half_day)
+			.where(
+				(LeaveType.is_lwp == 1)
+				& (LeaveApplication.docstatus == 1)
+				& (LeaveApplication.status == "Approved")
+				& (LeaveApplication.employee == employee)
+				& ((LeaveApplication.from_date <= date) & (date <= LeaveApplication.to_date))
+			)
 		)
-		if leave:
-			lwp = cint(leave[0][1]) and (lwp + 0.5) or (lwp + 1)
+
+		# if it's a holiday only include if leave type has "include holiday" enabled
+		if date in holidays:
+			query = query.where((LeaveType.include_holiday == "1"))
+		leaves = query.run(as_dict=True)
+
+		if leaves:
+			lwp += 0.5 if leaves[0].is_half_day else 1
+
 	return lwp
 
 
diff --git a/erpnext/payroll/doctype/employee_benefit_application/test_employee_benefit_application.py b/erpnext/payroll/doctype/employee_benefit_application/test_employee_benefit_application.py
index 02149ad..de8f9b6 100644
--- a/erpnext/payroll/doctype/employee_benefit_application/test_employee_benefit_application.py
+++ b/erpnext/payroll/doctype/employee_benefit_application/test_employee_benefit_application.py
@@ -3,6 +3,82 @@
 
 import unittest
 
+import frappe
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import add_days, date_diff, get_year_ending, get_year_start, getdate
 
-class TestEmployeeBenefitApplication(unittest.TestCase):
-	pass
+from erpnext.hr.doctype.employee.test_employee import make_employee
+from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list
+from erpnext.hr.doctype.leave_application.test_leave_application import get_first_sunday
+from erpnext.hr.utils import get_holiday_dates_for_employee
+from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import (
+	calculate_lwp,
+)
+from erpnext.payroll.doctype.employee_tax_exemption_declaration.test_employee_tax_exemption_declaration import (
+	create_payroll_period,
+)
+from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
+	make_holiday_list,
+	make_leave_application,
+)
+from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip
+from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
+
+
+class TestEmployeeBenefitApplication(FrappeTestCase):
+	def setUp(self):
+		date = getdate()
+		make_holiday_list(from_date=get_year_start(date), to_date=get_year_ending(date))
+
+	@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
+	def test_employee_benefit_application(self):
+		payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company")
+		employee = make_employee("test_employee_benefits@salary.com", company="_Test Company")
+		first_sunday = get_first_sunday("Salary Slip Test Holiday List")
+
+		leave_application = make_leave_application(
+			employee,
+			add_days(first_sunday, 1),
+			add_days(first_sunday, 3),
+			"Leave Without Pay",
+			half_day=1,
+			half_day_date=add_days(first_sunday, 1),
+			submit=True,
+		)
+
+		frappe.db.set_value("Leave Type", "Leave Without Pay", "include_holiday", 0)
+		salary_structure = make_salary_structure(
+			"Test Employee Benefits",
+			"Monthly",
+			other_details={"max_benefits": 100000},
+			include_flexi_benefits=True,
+			employee=employee,
+			payroll_period=payroll_period,
+		)
+		salary_slip = make_salary_slip(salary_structure.name, employee=employee, posting_date=getdate())
+		salary_slip.insert()
+		salary_slip.submit()
+
+		application = make_employee_benefit_application(
+			employee, payroll_period.name, date=leave_application.to_date
+		)
+		self.assertEqual(application.employee_benefits[0].max_benefit_amount, 15000)
+
+		holidays = get_holiday_dates_for_employee(employee, payroll_period.start_date, application.date)
+		working_days = date_diff(application.date, payroll_period.start_date) + 1
+		lwp = calculate_lwp(employee, payroll_period.start_date, holidays, working_days)
+		self.assertEqual(lwp, 2.5)
+
+
+def make_employee_benefit_application(employee, payroll_period, date):
+	frappe.db.delete("Employee Benefit Application")
+
+	return frappe.get_doc(
+		{
+			"doctype": "Employee Benefit Application",
+			"employee": employee,
+			"date": date,
+			"payroll_period": payroll_period,
+			"employee_benefits": [{"earning_component": "Medical Allowance", "amount": 1500}],
+		}
+	).insert()
diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py
index c0ef2ee..3d1d965 100644
--- a/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py
+++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py
@@ -33,7 +33,9 @@
 			self.total_declared_amount += flt(d.amount)
 
 	def set_total_exemption_amount(self):
-		self.total_exemption_amount = get_total_exemption_amount(self.declarations)
+		self.total_exemption_amount = flt(
+			get_total_exemption_amount(self.declarations), self.precision("total_exemption_amount")
+		)
 
 	def calculate_hra_exemption(self):
 		self.salary_structure_hra, self.annual_hra_exemption, self.monthly_hra_exemption = 0, 0, 0
@@ -41,9 +43,18 @@
 			hra_exemption = calculate_annual_eligible_hra_exemption(self)
 			if hra_exemption:
 				self.total_exemption_amount += hra_exemption["annual_exemption"]
-				self.salary_structure_hra = hra_exemption["hra_amount"]
-				self.annual_hra_exemption = hra_exemption["annual_exemption"]
-				self.monthly_hra_exemption = hra_exemption["monthly_exemption"]
+				self.total_exemption_amount = flt(
+					self.total_exemption_amount, self.precision("total_exemption_amount")
+				)
+				self.salary_structure_hra = flt(
+					hra_exemption["hra_amount"], self.precision("salary_structure_hra")
+				)
+				self.annual_hra_exemption = flt(
+					hra_exemption["annual_exemption"], self.precision("annual_hra_exemption")
+				)
+				self.monthly_hra_exemption = flt(
+					hra_exemption["monthly_exemption"], self.precision("monthly_hra_exemption")
+				)
 
 
 @frappe.whitelist()
diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py b/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py
index 1d90e73..2d8df35 100644
--- a/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py
+++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py
@@ -4,25 +4,28 @@
 import unittest
 
 import frappe
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import add_months, getdate
 
 import erpnext
 from erpnext.hr.doctype.employee.test_employee import make_employee
 from erpnext.hr.utils import DuplicateDeclarationError
 
 
-class TestEmployeeTaxExemptionDeclaration(unittest.TestCase):
+class TestEmployeeTaxExemptionDeclaration(FrappeTestCase):
 	def setUp(self):
-		make_employee("employee@taxexepmtion.com")
-		make_employee("employee1@taxexepmtion.com")
-		create_payroll_period()
+		make_employee("employee@taxexemption.com", company="_Test Company")
+		make_employee("employee1@taxexemption.com", company="_Test Company")
+		create_payroll_period(company="_Test Company")
 		create_exemption_category()
-		frappe.db.sql("""delete from `tabEmployee Tax Exemption Declaration`""")
+		frappe.db.delete("Employee Tax Exemption Declaration")
+		frappe.db.delete("Salary Structure Assignment")
 
 	def test_duplicate_category_in_declaration(self):
 		declaration = frappe.get_doc(
 			{
 				"doctype": "Employee Tax Exemption Declaration",
-				"employee": frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name"),
+				"employee": frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name"),
 				"company": erpnext.get_default_company(),
 				"payroll_period": "_Test Payroll Period",
 				"currency": erpnext.get_default_currency(),
@@ -46,7 +49,7 @@
 		declaration = frappe.get_doc(
 			{
 				"doctype": "Employee Tax Exemption Declaration",
-				"employee": frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name"),
+				"employee": frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name"),
 				"company": erpnext.get_default_company(),
 				"payroll_period": "_Test Payroll Period",
 				"currency": erpnext.get_default_currency(),
@@ -68,7 +71,7 @@
 		duplicate_declaration = frappe.get_doc(
 			{
 				"doctype": "Employee Tax Exemption Declaration",
-				"employee": frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name"),
+				"employee": frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name"),
 				"company": erpnext.get_default_company(),
 				"payroll_period": "_Test Payroll Period",
 				"currency": erpnext.get_default_currency(),
@@ -83,7 +86,7 @@
 		)
 		self.assertRaises(DuplicateDeclarationError, duplicate_declaration.insert)
 		duplicate_declaration.employee = frappe.get_value(
-			"Employee", {"user_id": "employee1@taxexepmtion.com"}, "name"
+			"Employee", {"user_id": "employee1@taxexemption.com"}, "name"
 		)
 		self.assertTrue(duplicate_declaration.insert)
 
@@ -91,7 +94,7 @@
 		declaration = frappe.get_doc(
 			{
 				"doctype": "Employee Tax Exemption Declaration",
-				"employee": frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name"),
+				"employee": frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name"),
 				"company": erpnext.get_default_company(),
 				"payroll_period": "_Test Payroll Period",
 				"currency": erpnext.get_default_currency(),
@@ -112,6 +115,298 @@
 
 		self.assertEqual(declaration.total_exemption_amount, 100000)
 
+	def test_india_hra_exemption(self):
+		# set country
+		current_country = frappe.flags.country
+		frappe.flags.country = "India"
+
+		setup_hra_exemption_prerequisites("Monthly")
+		employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name")
+
+		declaration = frappe.get_doc(
+			{
+				"doctype": "Employee Tax Exemption Declaration",
+				"employee": employee,
+				"company": "_Test Company",
+				"payroll_period": "_Test Payroll Period",
+				"currency": "INR",
+				"monthly_house_rent": 50000,
+				"rented_in_metro_city": 1,
+				"declarations": [
+					dict(
+						exemption_sub_category="_Test Sub Category",
+						exemption_category="_Test Category",
+						amount=80000,
+					),
+					dict(
+						exemption_sub_category="_Test1 Sub Category",
+						exemption_category="_Test Category",
+						amount=60000,
+					),
+				],
+			}
+		).insert()
+
+		# Monthly HRA received = 3000
+		# should set HRA exemption as per actual annual HRA because that's the minimum
+		self.assertEqual(declaration.monthly_hra_exemption, 3000)
+		self.assertEqual(declaration.annual_hra_exemption, 36000)
+		# 100000 Standard Exemption + 36000 HRA exemption
+		self.assertEqual(declaration.total_exemption_amount, 136000)
+
+		# reset
+		frappe.flags.country = current_country
+
+	def test_india_hra_exemption_with_daily_payroll_frequency(self):
+		# set country
+		current_country = frappe.flags.country
+		frappe.flags.country = "India"
+
+		setup_hra_exemption_prerequisites("Daily")
+		employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name")
+
+		declaration = frappe.get_doc(
+			{
+				"doctype": "Employee Tax Exemption Declaration",
+				"employee": employee,
+				"company": "_Test Company",
+				"payroll_period": "_Test Payroll Period",
+				"currency": "INR",
+				"monthly_house_rent": 170000,
+				"rented_in_metro_city": 1,
+				"declarations": [
+					dict(
+						exemption_sub_category="_Test1 Sub Category",
+						exemption_category="_Test Category",
+						amount=60000,
+					),
+				],
+			}
+		).insert()
+
+		# Daily HRA received = 3000
+		# should set HRA exemption as per (rent - 10% of Basic Salary), that's the minimum
+		self.assertEqual(declaration.monthly_hra_exemption, 17916.67)
+		self.assertEqual(declaration.annual_hra_exemption, 215000)
+		# 50000 Standard Exemption + 215000 HRA exemption
+		self.assertEqual(declaration.total_exemption_amount, 265000)
+
+		# reset
+		frappe.flags.country = current_country
+
+	def test_india_hra_exemption_with_weekly_payroll_frequency(self):
+		# set country
+		current_country = frappe.flags.country
+		frappe.flags.country = "India"
+
+		setup_hra_exemption_prerequisites("Weekly")
+		employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name")
+
+		declaration = frappe.get_doc(
+			{
+				"doctype": "Employee Tax Exemption Declaration",
+				"employee": employee,
+				"company": "_Test Company",
+				"payroll_period": "_Test Payroll Period",
+				"currency": "INR",
+				"monthly_house_rent": 170000,
+				"rented_in_metro_city": 1,
+				"declarations": [
+					dict(
+						exemption_sub_category="_Test1 Sub Category",
+						exemption_category="_Test Category",
+						amount=60000,
+					),
+				],
+			}
+		).insert()
+
+		# Weekly HRA received = 3000
+		# should set HRA exemption as per actual annual HRA because that's the minimum
+		self.assertEqual(declaration.monthly_hra_exemption, 13000)
+		self.assertEqual(declaration.annual_hra_exemption, 156000)
+		# 50000 Standard Exemption + 156000 HRA exemption
+		self.assertEqual(declaration.total_exemption_amount, 206000)
+
+		# reset
+		frappe.flags.country = current_country
+
+	def test_india_hra_exemption_with_fortnightly_payroll_frequency(self):
+		# set country
+		current_country = frappe.flags.country
+		frappe.flags.country = "India"
+
+		setup_hra_exemption_prerequisites("Fortnightly")
+		employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name")
+
+		declaration = frappe.get_doc(
+			{
+				"doctype": "Employee Tax Exemption Declaration",
+				"employee": employee,
+				"company": "_Test Company",
+				"payroll_period": "_Test Payroll Period",
+				"currency": "INR",
+				"monthly_house_rent": 170000,
+				"rented_in_metro_city": 1,
+				"declarations": [
+					dict(
+						exemption_sub_category="_Test1 Sub Category",
+						exemption_category="_Test Category",
+						amount=60000,
+					),
+				],
+			}
+		).insert()
+
+		# Fortnightly HRA received = 3000
+		# should set HRA exemption as per actual annual HRA because that's the minimum
+		self.assertEqual(declaration.monthly_hra_exemption, 6500)
+		self.assertEqual(declaration.annual_hra_exemption, 78000)
+		# 50000 Standard Exemption + 78000 HRA exemption
+		self.assertEqual(declaration.total_exemption_amount, 128000)
+
+		# reset
+		frappe.flags.country = current_country
+
+	def test_india_hra_exemption_with_bimonthly_payroll_frequency(self):
+		# set country
+		current_country = frappe.flags.country
+		frappe.flags.country = "India"
+
+		setup_hra_exemption_prerequisites("Bimonthly")
+		employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name")
+
+		declaration = frappe.get_doc(
+			{
+				"doctype": "Employee Tax Exemption Declaration",
+				"employee": employee,
+				"company": "_Test Company",
+				"payroll_period": "_Test Payroll Period",
+				"currency": "INR",
+				"monthly_house_rent": 50000,
+				"rented_in_metro_city": 1,
+				"declarations": [
+					dict(
+						exemption_sub_category="_Test Sub Category",
+						exemption_category="_Test Category",
+						amount=80000,
+					),
+					dict(
+						exemption_sub_category="_Test1 Sub Category",
+						exemption_category="_Test Category",
+						amount=60000,
+					),
+				],
+			}
+		).insert()
+
+		# Bimonthly HRA received = 3000
+		# should set HRA exemption as per actual annual HRA because that's the minimum
+		self.assertEqual(declaration.monthly_hra_exemption, 1500)
+		self.assertEqual(declaration.annual_hra_exemption, 18000)
+		# 100000 Standard Exemption + 18000 HRA exemption
+		self.assertEqual(declaration.total_exemption_amount, 118000)
+
+		# reset
+		frappe.flags.country = current_country
+
+	def test_india_hra_exemption_with_multiple_salary_structure_assignments(self):
+		from erpnext.payroll.doctype.salary_slip.test_salary_slip import create_tax_slab
+		from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
+			create_salary_structure_assignment,
+			make_salary_structure,
+		)
+
+		# set country
+		current_country = frappe.flags.country
+		frappe.flags.country = "India"
+
+		employee = make_employee("employee@taxexemption2.com", company="_Test Company")
+		payroll_period = create_payroll_period(name="_Test Payroll Period", company="_Test Company")
+
+		create_tax_slab(
+			payroll_period,
+			allow_tax_exemption=True,
+			currency="INR",
+			effective_date=getdate("2019-04-01"),
+			company="_Test Company",
+		)
+
+		frappe.db.set_value(
+			"Company", "_Test Company", {"basic_component": "Basic Salary", "hra_component": "HRA"}
+		)
+
+		# salary structure with base 50000, HRA 3000
+		make_salary_structure(
+			"Monthly Structure for HRA Exemption 1",
+			"Monthly",
+			employee=employee,
+			company="_Test Company",
+			currency="INR",
+			payroll_period=payroll_period.name,
+			from_date=payroll_period.start_date,
+		)
+
+		# salary structure with base 70000, HRA = base * 0.2 = 14000
+		salary_structure = make_salary_structure(
+			"Monthly Structure for HRA Exemption 2",
+			"Monthly",
+			employee=employee,
+			company="_Test Company",
+			currency="INR",
+			payroll_period=payroll_period.name,
+			from_date=payroll_period.start_date,
+			dont_submit=True,
+		)
+		for component_row in salary_structure.earnings:
+			if component_row.salary_component == "HRA":
+				component_row.amount = 0
+				component_row.amount_based_on_formula = 1
+				component_row.formula = "base * 0.2"
+				break
+
+		salary_structure.submit()
+
+		create_salary_structure_assignment(
+			employee,
+			salary_structure.name,
+			from_date=add_months(payroll_period.start_date, 6),
+			company="_Test Company",
+			currency="INR",
+			payroll_period=payroll_period.name,
+			base=70000,
+			allow_duplicate=True,
+		)
+
+		declaration = frappe.get_doc(
+			{
+				"doctype": "Employee Tax Exemption Declaration",
+				"employee": employee,
+				"company": "_Test Company",
+				"payroll_period": payroll_period.name,
+				"currency": "INR",
+				"monthly_house_rent": 50000,
+				"rented_in_metro_city": 1,
+				"declarations": [
+					dict(
+						exemption_sub_category="_Test1 Sub Category",
+						exemption_category="_Test Category",
+						amount=60000,
+					),
+				],
+			}
+		).insert()
+
+		# Monthly HRA received = 50000 * 6 months + 70000 * 6 months
+		# should set HRA exemption as per actual annual HRA because that's the minimum
+		self.assertEqual(declaration.monthly_hra_exemption, 8500)
+		self.assertEqual(declaration.annual_hra_exemption, 102000)
+		# 50000 Standard Exemption + 102000 HRA exemption
+		self.assertEqual(declaration.total_exemption_amount, 152000)
+
+		# reset
+		frappe.flags.country = current_country
+
 
 def create_payroll_period(**args):
 	args = frappe._dict(args)
@@ -163,3 +458,33 @@
 				"is_active": 1,
 			}
 		).insert()
+
+
+def setup_hra_exemption_prerequisites(frequency, employee=None):
+	from erpnext.payroll.doctype.salary_slip.test_salary_slip import create_tax_slab
+	from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
+
+	payroll_period = create_payroll_period(name="_Test Payroll Period", company="_Test Company")
+	if not employee:
+		employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name")
+
+	create_tax_slab(
+		payroll_period,
+		allow_tax_exemption=True,
+		currency="INR",
+		effective_date=getdate("2019-04-01"),
+		company="_Test Company",
+	)
+
+	make_salary_structure(
+		f"{frequency} Structure for HRA Exemption",
+		frequency,
+		employee=employee,
+		company="_Test Company",
+		currency="INR",
+		payroll_period=payroll_period,
+	)
+
+	frappe.db.set_value(
+		"Company", "_Test Company", {"basic_component": "Basic Salary", "hra_component": "HRA"}
+	)
diff --git a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py
index c52efab..b3b66b9 100644
--- a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py
+++ b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py
@@ -31,7 +31,9 @@
 			self.total_actual_amount += flt(d.amount)
 
 	def set_total_exemption_amount(self):
-		self.exemption_amount = get_total_exemption_amount(self.tax_exemption_proofs)
+		self.exemption_amount = flt(
+			get_total_exemption_amount(self.tax_exemption_proofs), self.precision("exemption_amount")
+		)
 
 	def calculate_hra_exemption(self):
 		self.monthly_hra_exemption, self.monthly_house_rent, self.total_eligible_hra_exemption = 0, 0, 0
@@ -39,6 +41,13 @@
 			hra_exemption = calculate_hra_exemption_for_period(self)
 			if hra_exemption:
 				self.exemption_amount += hra_exemption["total_eligible_hra_exemption"]
-				self.monthly_hra_exemption = hra_exemption["monthly_exemption"]
-				self.monthly_house_rent = hra_exemption["monthly_house_rent"]
-				self.total_eligible_hra_exemption = hra_exemption["total_eligible_hra_exemption"]
+				self.exemption_amount = flt(self.exemption_amount, self.precision("exemption_amount"))
+				self.monthly_hra_exemption = flt(
+					hra_exemption["monthly_exemption"], self.precision("monthly_hra_exemption")
+				)
+				self.monthly_house_rent = flt(
+					hra_exemption["monthly_house_rent"], self.precision("monthly_house_rent")
+				)
+				self.total_eligible_hra_exemption = flt(
+					hra_exemption["total_eligible_hra_exemption"], self.precision("total_eligible_hra_exemption")
+				)
diff --git a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/test_employee_tax_exemption_proof_submission.py b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/test_employee_tax_exemption_proof_submission.py
index 58b2c1a..416cf31 100644
--- a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/test_employee_tax_exemption_proof_submission.py
+++ b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/test_employee_tax_exemption_proof_submission.py
@@ -4,22 +4,26 @@
 import unittest
 
 import frappe
+from frappe.tests.utils import FrappeTestCase
 
+from erpnext.hr.doctype.employee.test_employee import make_employee
 from erpnext.payroll.doctype.employee_tax_exemption_declaration.test_employee_tax_exemption_declaration import (
 	create_exemption_category,
 	create_payroll_period,
+	setup_hra_exemption_prerequisites,
 )
 
 
-class TestEmployeeTaxExemptionProofSubmission(unittest.TestCase):
-	def setup(self):
-		make_employee("employee@proofsubmission.com")
-		create_payroll_period()
+class TestEmployeeTaxExemptionProofSubmission(FrappeTestCase):
+	def setUp(self):
+		make_employee("employee@proofsubmission.com", company="_Test Company")
+		create_payroll_period(company="_Test Company")
 		create_exemption_category()
-		frappe.db.sql("""delete from `tabEmployee Tax Exemption Proof Submission`""")
+		frappe.db.delete("Employee Tax Exemption Proof Submission")
+		frappe.db.delete("Salary Structure Assignment")
 
 	def test_exemption_amount_lesser_than_category_max(self):
-		declaration = frappe.get_doc(
+		proof = frappe.get_doc(
 			{
 				"doctype": "Employee Tax Exemption Proof Submission",
 				"employee": frappe.get_value("Employee", {"user_id": "employee@proofsubmission.com"}, "name"),
@@ -34,8 +38,8 @@
 				],
 			}
 		)
-		self.assertRaises(frappe.ValidationError, declaration.save)
-		declaration = frappe.get_doc(
+		self.assertRaises(frappe.ValidationError, proof.save)
+		proof = frappe.get_doc(
 			{
 				"doctype": "Employee Tax Exemption Proof Submission",
 				"payroll_period": "Test Payroll Period",
@@ -50,11 +54,11 @@
 				],
 			}
 		)
-		self.assertTrue(declaration.save)
-		self.assertTrue(declaration.submit)
+		self.assertTrue(proof.save)
+		self.assertTrue(proof.submit)
 
 	def test_duplicate_category_in_proof_submission(self):
-		declaration = frappe.get_doc(
+		proof = frappe.get_doc(
 			{
 				"doctype": "Employee Tax Exemption Proof Submission",
 				"employee": frappe.get_value("Employee", {"user_id": "employee@proofsubmission.com"}, "name"),
@@ -74,4 +78,59 @@
 				],
 			}
 		)
-		self.assertRaises(frappe.ValidationError, declaration.save)
+		self.assertRaises(frappe.ValidationError, proof.save)
+
+	def test_india_hra_exemption(self):
+		# set country
+		current_country = frappe.flags.country
+		frappe.flags.country = "India"
+
+		employee = frappe.get_value("Employee", {"user_id": "employee@proofsubmission.com"}, "name")
+		setup_hra_exemption_prerequisites("Monthly", employee)
+		payroll_period = frappe.db.get_value(
+			"Payroll Period", "_Test Payroll Period", ["start_date", "end_date"], as_dict=True
+		)
+
+		proof = frappe.get_doc(
+			{
+				"doctype": "Employee Tax Exemption Proof Submission",
+				"employee": employee,
+				"company": "_Test Company",
+				"payroll_period": "_Test Payroll Period",
+				"currency": "INR",
+				"house_rent_payment_amount": 600000,
+				"rented_in_metro_city": 1,
+				"rented_from_date": payroll_period.start_date,
+				"rented_to_date": payroll_period.end_date,
+				"tax_exemption_proofs": [
+					dict(
+						exemption_sub_category="_Test Sub Category",
+						exemption_category="_Test Category",
+						type_of_proof="Test Proof",
+						amount=100000,
+					),
+					dict(
+						exemption_sub_category="_Test1 Sub Category",
+						exemption_category="_Test Category",
+						type_of_proof="Test Proof",
+						amount=50000,
+					),
+				],
+			}
+		).insert()
+
+		self.assertEqual(proof.monthly_house_rent, 50000)
+
+		# Monthly HRA received = 3000
+		# should set HRA exemption as per actual annual HRA because that's the minimum
+		self.assertEqual(proof.monthly_hra_exemption, 3000)
+		self.assertEqual(proof.total_eligible_hra_exemption, 36000)
+
+		# total exemptions + house rent payment amount
+		self.assertEqual(proof.total_actual_amount, 750000)
+
+		# 100000 Standard Exemption + 36000 HRA exemption
+		self.assertEqual(proof.exemption_amount, 136000)
+
+		# reset
+		frappe.flags.country = current_country
diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js
index 62e183e..b06f350 100644
--- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js
@@ -40,30 +40,69 @@
 	},
 
 	refresh: function (frm) {
-		if (frm.doc.docstatus == 0) {
-			if (!frm.is_new()) {
+		if (frm.doc.docstatus === 0 && !frm.is_new()) {
+			frm.page.clear_primary_action();
+			frm.add_custom_button(__("Get Employees"),
+				function () {
+					frm.events.get_employee_details(frm);
+				}
+			).toggleClass("btn-primary", !(frm.doc.employees || []).length);
+		}
+
+		if (
+			(frm.doc.employees || []).length
+			&& !frappe.model.has_workflow(frm.doctype)
+			&& !cint(frm.doc.salary_slips_created)
+			&& (frm.doc.docstatus != 2)
+		) {
+			if (frm.doc.docstatus == 0) {
 				frm.page.clear_primary_action();
-				frm.add_custom_button(__("Get Employees"),
-					function () {
-						frm.events.get_employee_details(frm);
-					}
-				).toggleClass('btn-primary', !(frm.doc.employees || []).length);
-			}
-			if ((frm.doc.employees || []).length && !frappe.model.has_workflow(frm.doctype)) {
-				frm.page.clear_primary_action();
-				frm.page.set_primary_action(__('Create Salary Slips'), () => {
-					frm.save('Submit').then(() => {
+				frm.page.set_primary_action(__("Create Salary Slips"), () => {
+					frm.save("Submit").then(() => {
 						frm.page.clear_primary_action();
 						frm.refresh();
 						frm.events.refresh(frm);
 					});
 				});
+			} else if (frm.doc.docstatus == 1 && frm.doc.status == "Failed") {
+				frm.add_custom_button(__("Create Salary Slip"), function () {
+					frm.call("create_salary_slips", {}, () => {
+						frm.reload_doc();
+					});
+				}).addClass("btn-primary");
 			}
 		}
-		if (frm.doc.docstatus == 1) {
+
+		if (frm.doc.docstatus == 1 && frm.doc.status == "Submitted") {
 			if (frm.custom_buttons) frm.clear_custom_buttons();
 			frm.events.add_context_buttons(frm);
 		}
+
+		if (frm.doc.status == "Failed" && frm.doc.error_message) {
+			const issue = `<a id="jump_to_error" style="text-decoration: underline;">issue</a>`;
+			let process = (cint(frm.doc.salary_slips_created)) ? "submission" : "creation";
+
+			frm.dashboard.set_headline(
+				__("Salary Slip {0} failed. You can resolve the {1} and retry {0}.", [process, issue])
+			);
+
+			$("#jump_to_error").on("click", (e) => {
+				e.preventDefault();
+				frappe.utils.scroll_to(
+					frm.get_field("error_message").$wrapper,
+					true,
+					30
+				);
+			});
+		}
+
+		frappe.realtime.on("completed_salary_slip_creation", function() {
+			frm.reload_doc();
+		});
+
+		frappe.realtime.on("completed_salary_slip_submission", function() {
+			frm.reload_doc();
+		});
 	},
 
 	get_employee_details: function (frm) {
@@ -88,7 +127,7 @@
 			doc: frm.doc,
 			method: "create_salary_slips",
 			callback: function () {
-				frm.refresh();
+				frm.reload_doc();
 				frm.toolbar.refresh();
 			}
 		});
@@ -97,7 +136,7 @@
 	add_context_buttons: function (frm) {
 		if (frm.doc.salary_slips_submitted || (frm.doc.__onload && frm.doc.__onload.submitted_ss)) {
 			frm.events.add_bank_entry_button(frm);
-		} else if (frm.doc.salary_slips_created) {
+		} else if (frm.doc.salary_slips_created && frm.doc.status != 'Queued') {
 			frm.add_custom_button(__("Submit Salary Slip"), function () {
 				submit_salary_slip(frm);
 			}).addClass("btn-primary");
@@ -331,6 +370,7 @@
 				method: 'submit_salary_slips',
 				args: {},
 				callback: function () {
+					frm.reload_doc();
 					frm.events.refresh(frm);
 				},
 				doc: frm.doc,
diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.json b/erpnext/payroll/doctype/payroll_entry/payroll_entry.json
index 0444134..17882eb 100644
--- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.json
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.json
@@ -8,11 +8,11 @@
  "engine": "InnoDB",
  "field_order": [
   "section_break0",
-  "column_break0",
   "posting_date",
   "payroll_frequency",
   "company",
   "column_break1",
+  "status",
   "currency",
   "exchange_rate",
   "payroll_payable_account",
@@ -41,11 +41,14 @@
   "cost_center",
   "account",
   "payment_account",
-  "amended_from",
   "column_break_33",
   "bank_account",
   "salary_slips_created",
-  "salary_slips_submitted"
+  "salary_slips_submitted",
+  "failure_details_section",
+  "error_message",
+  "section_break_41",
+  "amended_from"
  ],
  "fields": [
   {
@@ -54,11 +57,6 @@
    "label": "Select Employees"
   },
   {
-   "fieldname": "column_break0",
-   "fieldtype": "Column Break",
-   "width": "50%"
-  },
-  {
    "default": "Today",
    "fieldname": "posting_date",
    "fieldtype": "Date",
@@ -231,6 +229,7 @@
    "fieldtype": "Check",
    "hidden": 1,
    "label": "Salary Slips Created",
+   "no_copy": 1,
    "read_only": 1
   },
   {
@@ -239,6 +238,7 @@
    "fieldtype": "Check",
    "hidden": 1,
    "label": "Salary Slips Submitted",
+   "no_copy": 1,
    "read_only": 1
   },
   {
@@ -284,15 +284,44 @@
    "label": "Payroll Payable Account",
    "options": "Account",
    "reqd": 1
+  },
+  {
+   "collapsible": 1,
+   "collapsible_depends_on": "error_message",
+   "depends_on": "eval:doc.status=='Failed';",
+   "fieldname": "failure_details_section",
+   "fieldtype": "Section Break",
+   "label": "Failure Details"
+  },
+  {
+   "depends_on": "eval:doc.status=='Failed';",
+   "fieldname": "error_message",
+   "fieldtype": "Small Text",
+   "label": "Error Message",
+   "no_copy": 1,
+   "read_only": 1
+  },
+  {
+   "fieldname": "section_break_41",
+   "fieldtype": "Section Break"
+  },
+  {
+   "fieldname": "status",
+   "fieldtype": "Select",
+   "label": "Status",
+   "options": "Draft\nSubmitted\nCancelled\nQueued\nFailed",
+   "print_hide": 1,
+   "read_only": 1
   }
  ],
  "icon": "fa fa-cog",
  "is_submittable": 1,
  "links": [],
- "modified": "2020-12-17 15:13:17.766210",
+ "modified": "2022-03-16 12:45:21.662765",
  "modified_by": "Administrator",
  "module": "Payroll",
  "name": "Payroll Entry",
+ "naming_rule": "Expression (old style)",
  "owner": "Administrator",
  "permissions": [
   {
@@ -308,5 +337,6 @@
   }
  ],
  "sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "states": []
 }
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
index 473fb0d..620fcad 100644
--- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
@@ -1,6 +1,7 @@
 # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
 # For license information, please see license.txt
 
+import json
 
 import frappe
 from dateutil.relativedelta import relativedelta
@@ -40,8 +41,10 @@
 
 	def validate(self):
 		self.number_of_employees = len(self.employees)
+		self.set_status()
 
 	def on_submit(self):
+		self.set_status(update=True, status="Submitted")
 		self.create_salary_slips()
 
 	def before_submit(self):
@@ -51,6 +54,15 @@
 			if self.validate_employee_attendance():
 				frappe.throw(_("Cannot Submit, Employees left to mark attendance"))
 
+	def set_status(self, status=None, update=False):
+		if not status:
+			status = {0: "Draft", 1: "Submitted", 2: "Cancelled"}[self.docstatus or 0]
+
+		if update:
+			self.db_set("status", status)
+		else:
+			self.status = status
+
 	def validate_employee_details(self):
 		emp_with_sal_slip = []
 		for employee_details in self.employees:
@@ -87,6 +99,8 @@
 		)
 		self.db_set("salary_slips_created", 0)
 		self.db_set("salary_slips_submitted", 0)
+		self.set_status(update=True, status="Cancelled")
+		self.db_set("error_message", "")
 
 	def get_emp_list(self):
 		"""
@@ -183,8 +197,20 @@
 					"currency": self.currency,
 				}
 			)
-			if len(employees) > 30:
-				frappe.enqueue(create_salary_slips_for_employees, timeout=600, employees=employees, args=args)
+			if len(employees) > 30 or frappe.flags.enqueue_payroll_entry:
+				self.db_set("status", "Queued")
+				frappe.enqueue(
+					create_salary_slips_for_employees,
+					timeout=600,
+					employees=employees,
+					args=args,
+					publish_progress=False,
+				)
+				frappe.msgprint(
+					_("Salary Slip creation is queued. It may take a few minutes"),
+					alert=True,
+					indicator="blue",
+				)
 			else:
 				create_salary_slips_for_employees(employees, args, publish_progress=False)
 				# since this method is called via frm.call this doc needs to be updated manually
@@ -214,13 +240,23 @@
 	@frappe.whitelist()
 	def submit_salary_slips(self):
 		self.check_permission("write")
-		ss_list = self.get_sal_slip_list(ss_status=0)
-		if len(ss_list) > 30:
+		salary_slips = self.get_sal_slip_list(ss_status=0)
+		if len(salary_slips) > 30 or frappe.flags.enqueue_payroll_entry:
+			self.db_set("status", "Queued")
 			frappe.enqueue(
-				submit_salary_slips_for_employees, timeout=600, payroll_entry=self, salary_slips=ss_list
+				submit_salary_slips_for_employees,
+				timeout=600,
+				payroll_entry=self,
+				salary_slips=salary_slips,
+				publish_progress=False,
+			)
+			frappe.msgprint(
+				_("Salary Slip submission is queued. It may take a few minutes"),
+				alert=True,
+				indicator="blue",
 			)
 		else:
-			submit_salary_slips_for_employees(self, ss_list, publish_progress=False)
+			submit_salary_slips_for_employees(self, salary_slips, publish_progress=False)
 
 	def email_salary_slip(self, submitted_ss):
 		if frappe.db.get_single_value("Payroll Settings", "email_salary_slip_to_employee"):
@@ -233,7 +269,11 @@
 		)
 
 		if not account:
-			frappe.throw(_("Please set account in Salary Component {0}").format(salary_component))
+			frappe.throw(
+				_("Please set account in Salary Component {0}").format(
+					get_link_to_form("Salary Component", salary_component)
+				)
+			)
 
 		return account
 
@@ -790,36 +830,80 @@
 	return response
 
 
+def log_payroll_failure(process, payroll_entry, error):
+	error_log = frappe.log_error(
+		title=_("Salary Slip {0} failed for Payroll Entry {1}").format(process, payroll_entry.name)
+	)
+	message_log = frappe.message_log.pop() if frappe.message_log else str(error)
+
+	try:
+		error_message = json.loads(message_log).get("message")
+	except Exception:
+		error_message = message_log
+
+	error_message += "\n" + _("Check Error Log {0} for more details.").format(
+		get_link_to_form("Error Log", error_log.name)
+	)
+
+	payroll_entry.db_set({"error_message": error_message, "status": "Failed"})
+
+
 def create_salary_slips_for_employees(employees, args, publish_progress=True):
-	salary_slips_exists_for = get_existing_salary_slips(employees, args)
-	count = 0
-	salary_slips_not_created = []
-	for emp in employees:
-		if emp not in salary_slips_exists_for:
-			args.update({"doctype": "Salary Slip", "employee": emp})
-			ss = frappe.get_doc(args)
-			ss.insert()
-			count += 1
-			if publish_progress:
-				frappe.publish_progress(
-					count * 100 / len(set(employees) - set(salary_slips_exists_for)),
-					title=_("Creating Salary Slips..."),
-				)
+	try:
+		payroll_entry = frappe.get_doc("Payroll Entry", args.payroll_entry)
+		salary_slips_exist_for = get_existing_salary_slips(employees, args)
+		count = 0
 
-		else:
-			salary_slips_not_created.append(emp)
+		for emp in employees:
+			if emp not in salary_slips_exist_for:
+				args.update({"doctype": "Salary Slip", "employee": emp})
+				frappe.get_doc(args).insert()
 
-	payroll_entry = frappe.get_doc("Payroll Entry", args.payroll_entry)
-	payroll_entry.db_set("salary_slips_created", 1)
-	payroll_entry.notify_update()
+				count += 1
+				if publish_progress:
+					frappe.publish_progress(
+						count * 100 / len(set(employees) - set(salary_slips_exist_for)),
+						title=_("Creating Salary Slips..."),
+					)
 
-	if salary_slips_not_created:
+		payroll_entry.db_set({"status": "Submitted", "salary_slips_created": 1, "error_message": ""})
+
+		if salary_slips_exist_for:
+			frappe.msgprint(
+				_(
+					"Salary Slips already exist for employees {}, and will not be processed by this payroll."
+				).format(frappe.bold(", ".join(emp for emp in salary_slips_exist_for))),
+				title=_("Message"),
+				indicator="orange",
+			)
+
+	except Exception as e:
+		frappe.db.rollback()
+		log_payroll_failure("creation", payroll_entry, e)
+
+	finally:
+		frappe.db.commit()  # nosemgrep
+		frappe.publish_realtime("completed_salary_slip_creation")
+
+
+def show_payroll_submission_status(submitted, unsubmitted, payroll_entry):
+	if not submitted and not unsubmitted:
 		frappe.msgprint(
 			_(
-				"Salary Slips already exists for employees {}, and will not be processed by this payroll."
-			).format(frappe.bold(", ".join([emp for emp in salary_slips_not_created]))),
-			title=_("Message"),
-			indicator="orange",
+				"No salary slip found to submit for the above selected criteria OR salary slip already submitted"
+			)
+		)
+	elif submitted and not unsubmitted:
+		frappe.msgprint(
+			_("Salary Slips submitted for period from {0} to {1}").format(
+				payroll_entry.start_date, payroll_entry.end_date
+			)
+		)
+	elif unsubmitted:
+		frappe.msgprint(
+			_("Could not submit some Salary Slips: {}").format(
+				", ".join(get_link_to_form("Salary Slip", entry) for entry in unsubmitted)
+			)
 		)
 
 
@@ -837,45 +921,41 @@
 
 
 def submit_salary_slips_for_employees(payroll_entry, salary_slips, publish_progress=True):
-	submitted_ss = []
-	not_submitted_ss = []
-	frappe.flags.via_payroll_entry = True
+	try:
+		submitted = []
+		unsubmitted = []
+		frappe.flags.via_payroll_entry = True
+		count = 0
 
-	count = 0
-	for ss in salary_slips:
-		ss_obj = frappe.get_doc("Salary Slip", ss[0])
-		if ss_obj.net_pay < 0:
-			not_submitted_ss.append(ss[0])
-		else:
-			try:
-				ss_obj.submit()
-				submitted_ss.append(ss_obj)
-			except frappe.ValidationError:
-				not_submitted_ss.append(ss[0])
+		for entry in salary_slips:
+			salary_slip = frappe.get_doc("Salary Slip", entry[0])
+			if salary_slip.net_pay < 0:
+				unsubmitted.append(entry[0])
+			else:
+				try:
+					salary_slip.submit()
+					submitted.append(salary_slip)
+				except frappe.ValidationError:
+					unsubmitted.append(entry[0])
 
-		count += 1
-		if publish_progress:
-			frappe.publish_progress(count * 100 / len(salary_slips), title=_("Submitting Salary Slips..."))
-	if submitted_ss:
-		payroll_entry.make_accrual_jv_entry()
-		frappe.msgprint(
-			_("Salary Slip submitted for period from {0} to {1}").format(ss_obj.start_date, ss_obj.end_date)
-		)
+			count += 1
+			if publish_progress:
+				frappe.publish_progress(count * 100 / len(salary_slips), title=_("Submitting Salary Slips..."))
 
-		payroll_entry.email_salary_slip(submitted_ss)
+		if submitted:
+			payroll_entry.make_accrual_jv_entry()
+			payroll_entry.email_salary_slip(submitted)
+			payroll_entry.db_set({"salary_slips_submitted": 1, "status": "Submitted", "error_message": ""})
 
-		payroll_entry.db_set("salary_slips_submitted", 1)
-		payroll_entry.notify_update()
+		show_payroll_submission_status(submitted, unsubmitted, payroll_entry)
 
-	if not submitted_ss and not not_submitted_ss:
-		frappe.msgprint(
-			_(
-				"No salary slip found to submit for the above selected criteria OR salary slip already submitted"
-			)
-		)
+	except Exception as e:
+		frappe.db.rollback()
+		log_payroll_failure("submission", payroll_entry, e)
 
-	if not_submitted_ss:
-		frappe.msgprint(_("Could not submit some Salary Slips"))
+	finally:
+		frappe.db.commit()  # nosemgrep
+		frappe.publish_realtime("completed_salary_slip_submission")
 
 	frappe.flags.via_payroll_entry = False
 
diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry_list.js b/erpnext/payroll/doctype/payroll_entry/payroll_entry_list.js
new file mode 100644
index 0000000..56390b7
--- /dev/null
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry_list.js
@@ -0,0 +1,18 @@
+// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+// License: GNU General Public License v3. See license.txt
+
+// render
+frappe.listview_settings['Payroll Entry'] = {
+	has_indicator_for_draft: 1,
+	get_indicator: function(doc) {
+		var status_color = {
+			'Draft': 'red',
+			'Submitted': 'blue',
+			'Queued': 'orange',
+			'Failed': 'red',
+			'Cancelled': 'red'
+
+		};
+		return [__(doc.status), status_color[doc.status], 'status,=,'+doc.status];
+	}
+};
diff --git a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py
index fda0fcf..0363a0c 100644
--- a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py
+++ b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py
@@ -5,6 +5,7 @@
 
 import frappe
 from dateutil.relativedelta import relativedelta
+from frappe.tests.utils import FrappeTestCase
 from frappe.utils import add_months
 
 import erpnext
@@ -22,10 +23,9 @@
 from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_end_date, get_start_end_dates
 from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
 	create_account,
-	get_salary_component_account,
 	make_deduction_salary_component,
 	make_earning_salary_component,
-	make_employee_salary_slip,
+	set_salary_component_account,
 )
 from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
 	create_salary_structure_assignment,
@@ -35,13 +35,7 @@
 test_dependencies = ["Holiday List"]
 
 
-class TestPayrollEntry(unittest.TestCase):
-	@classmethod
-	def setUpClass(cls):
-		frappe.db.set_value(
-			"Company", erpnext.get_default_company(), "default_holiday_list", "_Test Holiday List"
-		)
-
+class TestPayrollEntry(FrappeTestCase):
 	def setUp(self):
 		for dt in [
 			"Salary Slip",
@@ -52,81 +46,72 @@
 			"Salary Structure Assignment",
 			"Payroll Employee Detail",
 			"Additional Salary",
+			"Loan",
 		]:
-			frappe.db.sql("delete from `tab%s`" % dt)
+			frappe.db.delete(dt)
 
 		make_earning_salary_component(setup=True, company_list=["_Test Company"])
 		make_deduction_salary_component(setup=True, test_tax=False, company_list=["_Test Company"])
 
+		frappe.db.set_value("Company", "_Test Company", "default_holiday_list", "_Test Holiday List")
 		frappe.db.set_value("Payroll Settings", None, "email_salary_slip_to_employee", 0)
 
-	def test_payroll_entry(self):  # pylint: disable=no-self-use
-		company = erpnext.get_default_company()
-		for data in frappe.get_all("Salary Component", fields=["name"]):
-			if not frappe.db.get_value(
-				"Salary Component Account", {"parent": data.name, "company": company}, "name"
-			):
-				get_salary_component_account(data.name)
-
-		employee = frappe.db.get_value("Employee", {"company": company})
-		company_doc = frappe.get_doc("Company", company)
-		make_salary_structure(
-			"_Test Salary Structure",
-			"Monthly",
-			employee,
-			company=company,
-			currency=company_doc.default_currency,
+		# set default payable account
+		default_account = frappe.db.get_value(
+			"Company", "_Test Company", "default_payroll_payable_account"
 		)
-		dates = get_start_end_dates("Monthly", nowdate())
-		if not frappe.db.get_value(
-			"Salary Slip", {"start_date": dates.start_date, "end_date": dates.end_date}
-		):
-			make_payroll_entry(
-				start_date=dates.start_date,
-				end_date=dates.end_date,
-				payable_account=company_doc.default_payroll_payable_account,
-				currency=company_doc.default_currency,
+		if not default_account or default_account != "_Test Payroll Payable - _TC":
+			create_account(
+				account_name="_Test Payroll Payable",
+				company="_Test Company",
+				parent_account="Current Liabilities - _TC",
+				account_type="Payable",
+			)
+			frappe.db.set_value(
+				"Company", "_Test Company", "default_payroll_payable_account", "_Test Payroll Payable - _TC"
 			)
 
-	def test_multi_currency_payroll_entry(self):  # pylint: disable=no-self-use
-		company = erpnext.get_default_company()
-		employee = make_employee("test_muti_currency_employee@payroll.com", company=company)
-		for data in frappe.get_all("Salary Component", fields=["name"]):
-			if not frappe.db.get_value(
-				"Salary Component Account", {"parent": data.name, "company": company}, "name"
-			):
-				get_salary_component_account(data.name)
+	def test_payroll_entry(self):
+		company = frappe.get_doc("Company", "_Test Company")
+		employee = frappe.db.get_value("Employee", {"company": "_Test Company"})
+		setup_salary_structure(employee, company)
 
-		company_doc = frappe.get_doc("Company", company)
-		salary_structure = make_salary_structure(
-			"_Test Multi Currency Salary Structure", "Monthly", company=company, currency="USD"
+		dates = get_start_end_dates("Monthly", nowdate())
+		make_payroll_entry(
+			start_date=dates.start_date,
+			end_date=dates.end_date,
+			payable_account=company.default_payroll_payable_account,
+			currency=company.default_currency,
+			company=company.name,
 		)
-		create_salary_structure_assignment(
-			employee, salary_structure.name, company=company, currency="USD"
+
+	def test_multi_currency_payroll_entry(self):
+		company = frappe.get_doc("Company", "_Test Company")
+		employee = make_employee(
+			"test_muti_currency_employee@payroll.com", company=company.name, department="Accounts - _TC"
 		)
-		frappe.db.sql(
-			"""delete from `tabSalary Slip` where employee=%s""",
-			(frappe.db.get_value("Employee", {"user_id": "test_muti_currency_employee@payroll.com"})),
-		)
-		salary_slip = get_salary_slip(
-			"test_muti_currency_employee@payroll.com", "Monthly", "_Test Multi Currency Salary Structure"
-		)
+		salary_structure = "_Test Multi Currency Salary Structure"
+		setup_salary_structure(employee, company, "USD", salary_structure)
+
 		dates = get_start_end_dates("Monthly", nowdate())
 		payroll_entry = make_payroll_entry(
 			start_date=dates.start_date,
 			end_date=dates.end_date,
-			payable_account=company_doc.default_payroll_payable_account,
+			payable_account=company.default_payroll_payable_account,
 			currency="USD",
 			exchange_rate=70,
+			company=company.name,
+			cost_center="Main - _TC",
 		)
 		payroll_entry.make_payment_entry()
 
-		salary_slip.load_from_db()
+		salary_slip = frappe.db.get_value("Salary Slip", {"payroll_entry": payroll_entry.name}, "name")
+		salary_slip = frappe.get_doc("Salary Slip", salary_slip)
 
+		payroll_entry.reload()
 		payroll_je = salary_slip.journal_entry
 		if payroll_je:
 			payroll_je_doc = frappe.get_doc("Journal Entry", payroll_je)
-
 			self.assertEqual(salary_slip.base_gross_pay, payroll_je_doc.total_debit)
 			self.assertEqual(salary_slip.base_gross_pay, payroll_je_doc.total_credit)
 
@@ -139,27 +124,15 @@
 			(payroll_entry.name),
 			as_dict=1,
 		)
-
 		self.assertEqual(salary_slip.base_net_pay, payment_entry[0].total_debit)
 		self.assertEqual(salary_slip.base_net_pay, payment_entry[0].total_credit)
 
-	def test_payroll_entry_with_employee_cost_center(self):  # pylint: disable=no-self-use
-		for data in frappe.get_all("Salary Component", fields=["name"]):
-			if not frappe.db.get_value(
-				"Salary Component Account", {"parent": data.name, "company": "_Test Company"}, "name"
-			):
-				get_salary_component_account(data.name)
-
+	def test_payroll_entry_with_employee_cost_center(self):
 		if not frappe.db.exists("Department", "cc - _TC"):
 			frappe.get_doc(
 				{"doctype": "Department", "department_name": "cc", "company": "_Test Company"}
 			).insert()
 
-		frappe.db.sql("""delete from `tabEmployee` where employee_name='test_employee1@example.com' """)
-		frappe.db.sql("""delete from `tabEmployee` where employee_name='test_employee2@example.com' """)
-		frappe.db.sql("""delete from `tabSalary Structure` where name='_Test Salary Structure 1' """)
-		frappe.db.sql("""delete from `tabSalary Structure` where name='_Test Salary Structure 2' """)
-
 		employee1 = make_employee(
 			"test_employee1@example.com",
 			payroll_cost_center="_Test Cost Center - _TC",
@@ -170,38 +143,15 @@
 			"test_employee2@example.com", department="cc - _TC", company="_Test Company"
 		)
 
-		if not frappe.db.exists("Account", "_Test Payroll Payable - _TC"):
-			create_account(
-				account_name="_Test Payroll Payable",
-				company="_Test Company",
-				parent_account="Current Liabilities - _TC",
-				account_type="Payable",
-			)
+		company = frappe.get_doc("Company", "_Test Company")
+		setup_salary_structure(employee1, company)
 
-		if (
-			not frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account")
-			or frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account")
-			!= "_Test Payroll Payable - _TC"
-		):
-			frappe.db.set_value(
-				"Company", "_Test Company", "default_payroll_payable_account", "_Test Payroll Payable - _TC"
-			)
-		currency = frappe.db.get_value("Company", "_Test Company", "default_currency")
-
-		make_salary_structure(
-			"_Test Salary Structure 1",
-			"Monthly",
-			employee1,
-			company="_Test Company",
-			currency=currency,
-			test_tax=False,
-		)
 		ss = make_salary_structure(
 			"_Test Salary Structure 2",
 			"Monthly",
 			employee2,
 			company="_Test Company",
-			currency=currency,
+			currency=company.default_currency,
 			test_tax=False,
 		)
 
@@ -220,42 +170,38 @@
 		ssa_doc.append(
 			"payroll_cost_centers", {"cost_center": "_Test Cost Center 2 - _TC", "percentage": 40}
 		)
-
 		ssa_doc.save()
 
 		dates = get_start_end_dates("Monthly", nowdate())
-		if not frappe.db.get_value(
-			"Salary Slip", {"start_date": dates.start_date, "end_date": dates.end_date}
-		):
-			pe = make_payroll_entry(
-				start_date=dates.start_date,
-				end_date=dates.end_date,
-				payable_account="_Test Payroll Payable - _TC",
-				currency=frappe.db.get_value("Company", "_Test Company", "default_currency"),
-				department="cc - _TC",
-				company="_Test Company",
-				payment_account="Cash - _TC",
-				cost_center="Main - _TC",
-			)
-			je = frappe.db.get_value("Salary Slip", {"payroll_entry": pe.name}, "journal_entry")
-			je_entries = frappe.db.sql(
-				"""
-				select account, cost_center, debit, credit
-				from `tabJournal Entry Account`
-				where parent=%s
-				order by account, cost_center
-			""",
-				je,
-			)
-			expected_je = (
-				("_Test Payroll Payable - _TC", "Main - _TC", 0.0, 155600.0),
-				("Salary - _TC", "_Test Cost Center - _TC", 124800.0, 0.0),
-				("Salary - _TC", "_Test Cost Center 2 - _TC", 31200.0, 0.0),
-				("Salary Deductions - _TC", "_Test Cost Center - _TC", 0.0, 320.0),
-				("Salary Deductions - _TC", "_Test Cost Center 2 - _TC", 0.0, 80.0),
-			)
+		pe = make_payroll_entry(
+			start_date=dates.start_date,
+			end_date=dates.end_date,
+			payable_account="_Test Payroll Payable - _TC",
+			currency=frappe.db.get_value("Company", "_Test Company", "default_currency"),
+			department="cc - _TC",
+			company="_Test Company",
+			payment_account="Cash - _TC",
+			cost_center="Main - _TC",
+		)
+		je = frappe.db.get_value("Salary Slip", {"payroll_entry": pe.name}, "journal_entry")
+		je_entries = frappe.db.sql(
+			"""
+			select account, cost_center, debit, credit
+			from `tabJournal Entry Account`
+			where parent=%s
+			order by account, cost_center
+		""",
+			je,
+		)
+		expected_je = (
+			("_Test Payroll Payable - _TC", "Main - _TC", 0.0, 155600.0),
+			("Salary - _TC", "_Test Cost Center - _TC", 124800.0, 0.0),
+			("Salary - _TC", "_Test Cost Center 2 - _TC", 31200.0, 0.0),
+			("Salary Deductions - _TC", "_Test Cost Center - _TC", 0.0, 320.0),
+			("Salary Deductions - _TC", "_Test Cost Center 2 - _TC", 0.0, 80.0),
+		)
 
-			self.assertEqual(je_entries, expected_je)
+		self.assertEqual(je_entries, expected_je)
 
 	def test_get_end_date(self):
 		self.assertEqual(get_end_date("2017-01-01", "monthly"), {"end_date": "2017-01-31"})
@@ -268,31 +214,22 @@
 		self.assertEqual(get_end_date("2017-02-15", "daily"), {"end_date": "2017-02-15"})
 
 	def test_loan(self):
-		branch = "Test Employee Branch"
-		applicant = make_employee("test_employee@loan.com", company="_Test Company")
 		company = "_Test Company"
-		holiday_list = make_holiday("test holiday for loan")
-
-		company_doc = frappe.get_doc("Company", company)
-		if not company_doc.default_payroll_payable_account:
-			company_doc.default_payroll_payable_account = frappe.db.get_value(
-				"Account", {"company": company, "root_type": "Liability", "account_type": ""}, "name"
-			)
-			company_doc.save()
+		branch = "Test Employee Branch"
 
 		if not frappe.db.exists("Branch", branch):
 			frappe.get_doc({"doctype": "Branch", "branch": branch}).insert()
+		holiday_list = make_holiday("test holiday for loan")
 
-		employee_doc = frappe.get_doc("Employee", applicant)
-		employee_doc.branch = branch
-		employee_doc.holiday_list = holiday_list
-		employee_doc.save()
+		applicant = make_employee(
+			"test_employee@loan.com", company="_Test Company", branch=branch, holiday_list=holiday_list
+		)
+		company_doc = frappe.get_doc("Company", company)
 
-		salary_structure = "Test Salary Structure for Loan"
 		make_salary_structure(
-			salary_structure,
+			"Test Salary Structure for Loan",
 			"Monthly",
-			employee=employee_doc.name,
+			employee=applicant,
 			company="_Test Company",
 			currency=company_doc.default_currency,
 		)
@@ -353,11 +290,110 @@
 				self.assertEqual(row.principal_amount, principal_amount)
 				self.assertEqual(row.total_payment, interest_amount + principal_amount)
 
-		if salary_slip.docstatus == 0:
-			frappe.delete_doc("Salary Slip", name)
+	def test_salary_slip_operation_queueing(self):
+		company = "_Test Company"
+		company_doc = frappe.get_doc("Company", company)
+		employee = make_employee("test_employee@payroll.com", company=company)
+		setup_salary_structure(employee, company_doc)
+
+		# enqueue salary slip creation via payroll entry
+		# Payroll Entry status should change to Queued
+		dates = get_start_end_dates("Monthly", nowdate())
+		payroll_entry = get_payroll_entry(
+			start_date=dates.start_date,
+			end_date=dates.end_date,
+			payable_account=company_doc.default_payroll_payable_account,
+			currency=company_doc.default_currency,
+			company=company_doc.name,
+			cost_center="Main - _TC",
+		)
+		frappe.flags.enqueue_payroll_entry = True
+		payroll_entry.submit()
+		payroll_entry.reload()
+
+		self.assertEqual(payroll_entry.status, "Queued")
+		frappe.flags.enqueue_payroll_entry = False
+
+	def test_salary_slip_operation_failure(self):
+		company = "_Test Company"
+		company_doc = frappe.get_doc("Company", company)
+		employee = make_employee("test_employee@payroll.com", company=company)
+
+		salary_structure = make_salary_structure(
+			"_Test Salary Structure",
+			"Monthly",
+			employee,
+			company=company,
+			currency=company_doc.default_currency,
+		)
+
+		# reset account in component to test submission failure
+		component = frappe.get_doc("Salary Component", salary_structure.earnings[0].salary_component)
+		component.accounts = []
+		component.save()
+
+		# salary slip submission via payroll entry
+		# Payroll Entry status should change to Failed because of the missing account setup
+		dates = get_start_end_dates("Monthly", nowdate())
+		payroll_entry = get_payroll_entry(
+			start_date=dates.start_date,
+			end_date=dates.end_date,
+			payable_account=company_doc.default_payroll_payable_account,
+			currency=company_doc.default_currency,
+			company=company_doc.name,
+			cost_center="Main - _TC",
+		)
+
+		# set employee as Inactive to check creation failure
+		frappe.db.set_value("Employee", employee, "status", "Inactive")
+		payroll_entry.submit()
+		payroll_entry.reload()
+		self.assertEqual(payroll_entry.status, "Failed")
+		self.assertIsNotNone(payroll_entry.error_message)
+
+		frappe.db.set_value("Employee", employee, "status", "Active")
+		payroll_entry.submit()
+		payroll_entry.submit_salary_slips()
+
+		payroll_entry.reload()
+		self.assertEqual(payroll_entry.status, "Failed")
+		self.assertIsNotNone(payroll_entry.error_message)
+
+		# set accounts
+		for data in frappe.get_all("Salary Component", pluck="name"):
+			set_salary_component_account(data, company_list=[company])
+
+		# Payroll Entry successful, status should change to Submitted
+		payroll_entry.submit_salary_slips()
+		payroll_entry.reload()
+
+		self.assertEqual(payroll_entry.status, "Submitted")
+		self.assertEqual(payroll_entry.error_message, "")
+
+	def test_payroll_entry_status(self):
+		company = "_Test Company"
+		company_doc = frappe.get_doc("Company", company)
+		employee = make_employee("test_employee@payroll.com", company=company)
+
+		setup_salary_structure(employee, company_doc)
+
+		dates = get_start_end_dates("Monthly", nowdate())
+		payroll_entry = get_payroll_entry(
+			start_date=dates.start_date,
+			end_date=dates.end_date,
+			payable_account=company_doc.default_payroll_payable_account,
+			currency=company_doc.default_currency,
+			company=company_doc.name,
+			cost_center="Main - _TC",
+		)
+		payroll_entry.submit()
+		self.assertEqual(payroll_entry.status, "Submitted")
+
+		payroll_entry.cancel()
+		self.assertEqual(payroll_entry.status, "Cancelled")
 
 
-def make_payroll_entry(**args):
+def get_payroll_entry(**args):
 	args = frappe._dict(args)
 
 	payroll_entry = frappe.new_doc("Payroll Entry")
@@ -380,8 +416,17 @@
 		payroll_entry.payment_account = args.payment_account
 
 	payroll_entry.fill_employee_details()
-	payroll_entry.save()
-	payroll_entry.create_salary_slips()
+	payroll_entry.insert()
+
+	# Commit so that the first salary slip creation failure does not rollback the Payroll Entry insert.
+	frappe.db.commit()  # nosemgrep
+
+	return payroll_entry
+
+
+def make_payroll_entry(**args):
+	payroll_entry = get_payroll_entry(**args)
+	payroll_entry.submit()
 	payroll_entry.submit_salary_slips()
 	if payroll_entry.get_sal_slip_list(ss_status=1):
 		payroll_entry.make_payment_entry()
@@ -423,10 +468,17 @@
 	return holiday_list_name
 
 
-def get_salary_slip(user, period, salary_structure):
-	salary_slip = make_employee_salary_slip(user, period, salary_structure)
-	salary_slip.exchange_rate = 70
-	salary_slip.calculate_net_pay()
-	salary_slip.db_update()
+def setup_salary_structure(employee, company_doc, currency=None, salary_structure=None):
+	for data in frappe.get_all("Salary Component", pluck="name"):
+		if not frappe.db.get_value(
+			"Salary Component Account", {"parent": data, "company": company_doc.name}, "name"
+		):
+			set_salary_component_account(data)
 
-	return salary_slip
+	make_salary_structure(
+		salary_structure or "_Test Salary Structure",
+		"Monthly",
+		employee,
+		company=company_doc.name,
+		currency=(currency or company_doc.default_currency),
+	)
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index f4f8415..6a35985 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -29,6 +29,9 @@
 	calculate_amounts,
 	create_repayment_entry,
 )
+from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import (
+	process_loan_interest_accrual_for_term_loans,
+)
 from erpnext.payroll.doctype.additional_salary.additional_salary import get_additional_salaries
 from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import (
 	get_benefit_component_amount,
@@ -462,37 +465,14 @@
 		)
 
 		for d in range(working_days):
-			dt = add_days(cstr(getdate(self.start_date)), d)
-			leave = frappe.db.sql(
-				"""
-				SELECT t1.name,
-					CASE WHEN (t1.half_day_date = %(dt)s or t1.to_date = t1.from_date)
-					THEN t1.half_day else 0 END,
-					t2.is_ppl,
-					t2.fraction_of_daily_salary_per_leave
-				FROM `tabLeave Application` t1, `tabLeave Type` t2
-				WHERE t2.name = t1.leave_type
-				AND (t2.is_lwp = 1 or t2.is_ppl = 1)
-				AND t1.docstatus = 1
-				AND t1.employee = %(employee)s
-				AND ifnull(t1.salary_slip, '') = ''
-				AND CASE
-					WHEN t2.include_holiday != 1
-						THEN %(dt)s not in ('{0}') and %(dt)s between from_date and to_date
-					WHEN t2.include_holiday
-						THEN %(dt)s between from_date and to_date
-					END
-				""".format(
-					holidays
-				),
-				{"employee": self.employee, "dt": dt},
-			)
+			date = add_days(cstr(getdate(self.start_date)), d)
+			leave = get_lwp_or_ppl_for_date(date, self.employee, holidays)
 
 			if leave:
 				equivalent_lwp_count = 0
-				is_half_day_leave = cint(leave[0][1])
-				is_partially_paid_leave = cint(leave[0][2])
-				fraction_of_daily_salary_per_leave = flt(leave[0][3])
+				is_half_day_leave = cint(leave[0].is_half_day)
+				is_partially_paid_leave = cint(leave[0].is_ppl)
+				fraction_of_daily_salary_per_leave = flt(leave[0].fraction_of_daily_salary_per_leave)
 
 				equivalent_lwp_count = (1 - daily_wages_fraction_for_half_day) if is_half_day_leave else 1
 
@@ -1364,9 +1344,9 @@
 			self.total_loan_repayment += payment.total_payment
 
 	def get_loan_details(self):
-		return frappe.get_all(
+		loan_details = frappe.get_all(
 			"Loan",
-			fields=["name", "interest_income_account", "loan_account", "loan_type"],
+			fields=["name", "interest_income_account", "loan_account", "loan_type", "is_term_loan"],
 			filters={
 				"applicant": self.employee,
 				"docstatus": 1,
@@ -1375,6 +1355,15 @@
 			},
 		)
 
+		if loan_details:
+			for loan in loan_details:
+				if loan.is_term_loan:
+					process_loan_interest_accrual_for_term_loans(
+						posting_date=self.posting_date, loan_type=loan.loan_type, loan=loan.name
+					)
+
+		return loan_details
+
 	def make_loan_repayment_entry(self):
 		payroll_payable_account = get_payroll_payable_account(self.company, self.payroll_entry)
 		for loan in self.loans:
@@ -1730,3 +1719,46 @@
 	except Exception as e:
 		frappe.throw(_("Error in formula or condition: {0} in Income Tax Slab").format(e))
 		raise
+
+
+def get_lwp_or_ppl_for_date(date, employee, holidays):
+	LeaveApplication = frappe.qb.DocType("Leave Application")
+	LeaveType = frappe.qb.DocType("Leave Type")
+
+	is_half_day = (
+		frappe.qb.terms.Case()
+		.when(
+			(
+				(LeaveApplication.half_day_date == date)
+				| (LeaveApplication.from_date == LeaveApplication.to_date)
+			),
+			LeaveApplication.half_day,
+		)
+		.else_(0)
+	).as_("is_half_day")
+
+	query = (
+		frappe.qb.from_(LeaveApplication)
+		.inner_join(LeaveType)
+		.on((LeaveType.name == LeaveApplication.leave_type))
+		.select(
+			LeaveApplication.name,
+			LeaveType.is_ppl,
+			LeaveType.fraction_of_daily_salary_per_leave,
+			(is_half_day),
+		)
+		.where(
+			(((LeaveType.is_lwp == 1) | (LeaveType.is_ppl == 1)))
+			& (LeaveApplication.docstatus == 1)
+			& (LeaveApplication.status == "Approved")
+			& (LeaveApplication.employee == employee)
+			& ((LeaveApplication.salary_slip.isnull()) | (LeaveApplication.salary_slip == ""))
+			& ((LeaveApplication.from_date <= date) & (date <= LeaveApplication.to_date))
+		)
+	)
+
+	# if it's a holiday only include if leave type has "include holiday" enabled
+	if date in holidays:
+		query = query.where((LeaveType.include_holiday == "1"))
+
+	return query.run(as_dict=True)
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index 60ba2d9..a8b6bb5 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -49,7 +49,7 @@
 		"Payroll Settings", {"payroll_based_on": "Attendance", "daily_wages_fraction_for_half_day": 0.75}
 	)
 	def test_payment_days_based_on_attendance(self):
-		no_of_days = self.get_no_of_days()
+		no_of_days = get_no_of_days()
 
 		emp_id = make_employee("test_payment_days_based_on_attendance@salary.com")
 		frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"})
@@ -128,7 +128,7 @@
 		},
 	)
 	def test_payment_days_for_mid_joinee_including_holidays(self):
-		no_of_days = self.get_no_of_days()
+		no_of_days = get_no_of_days()
 		month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
 
 		new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com")
@@ -196,7 +196,7 @@
 		# tests mid month joining and relieving along with unmarked days
 		from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
 
-		no_of_days = self.get_no_of_days()
+		no_of_days = get_no_of_days()
 		month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
 
 		new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com")
@@ -236,7 +236,7 @@
 	def test_payment_days_for_mid_joinee_excluding_holidays(self):
 		from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
 
-		no_of_days = self.get_no_of_days()
+		no_of_days = get_no_of_days()
 		month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
 
 		new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com")
@@ -267,7 +267,7 @@
 
 	@change_settings("Payroll Settings", {"payroll_based_on": "Leave"})
 	def test_payment_days_based_on_leave_application(self):
-		no_of_days = self.get_no_of_days()
+		no_of_days = get_no_of_days()
 
 		emp_id = make_employee("test_payment_days_based_on_leave_application@salary.com")
 		frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"})
@@ -366,7 +366,7 @@
 		salary_slip.submit()
 		salary_slip.reload()
 
-		no_of_days = self.get_no_of_days()
+		no_of_days = get_no_of_days()
 		days_in_month = no_of_days[0]
 		no_of_holidays = no_of_days[1]
 
@@ -441,7 +441,7 @@
 
 	@change_settings("Payroll Settings", {"include_holidays_in_total_working_days": 1})
 	def test_salary_slip_with_holidays_included(self):
-		no_of_days = self.get_no_of_days()
+		no_of_days = get_no_of_days()
 		make_employee("test_salary_slip_with_holidays_included@salary.com")
 		frappe.db.set_value(
 			"Employee",
@@ -473,7 +473,7 @@
 
 	@change_settings("Payroll Settings", {"include_holidays_in_total_working_days": 0})
 	def test_salary_slip_with_holidays_excluded(self):
-		no_of_days = self.get_no_of_days()
+		no_of_days = get_no_of_days()
 		make_employee("test_salary_slip_with_holidays_excluded@salary.com")
 		frappe.db.set_value(
 			"Employee",
@@ -510,7 +510,7 @@
 			create_salary_structure_assignment,
 		)
 
-		no_of_days = self.get_no_of_days()
+		no_of_days = get_no_of_days()
 
 		# set joinng date in the same month
 		employee = make_employee("test_payment_days@salary.com")
@@ -984,17 +984,18 @@
 		activity_type.wage_rate = 25
 		activity_type.save()
 
-	def get_no_of_days(self):
-		no_of_days_in_month = calendar.monthrange(getdate(nowdate()).year, getdate(nowdate()).month)
-		no_of_holidays_in_month = len(
-			[
-				1
-				for i in calendar.monthcalendar(getdate(nowdate()).year, getdate(nowdate()).month)
-				if i[6] != 0
-			]
-		)
 
-		return [no_of_days_in_month[1], no_of_holidays_in_month]
+def get_no_of_days():
+	no_of_days_in_month = calendar.monthrange(getdate(nowdate()).year, getdate(nowdate()).month)
+	no_of_holidays_in_month = len(
+		[
+			1
+			for i in calendar.monthcalendar(getdate(nowdate()).year, getdate(nowdate()).month)
+			if i[6] != 0
+		]
+	)
+
+	return [no_of_days_in_month[1], no_of_holidays_in_month]
 
 
 def make_employee_salary_slip(user, payroll_frequency, salary_structure=None, posting_date=None):
@@ -1050,10 +1051,10 @@
 		doc.update(salary_component)
 		doc.insert()
 
-		get_salary_component_account(doc, company_list)
+		set_salary_component_account(doc, company_list)
 
 
-def get_salary_component_account(sal_comp, company_list=None):
+def set_salary_component_account(sal_comp, company_list=None):
 	company = erpnext.get_default_company()
 
 	if company_list and company not in company_list:
@@ -1136,6 +1137,7 @@
 					"pay_against_benefit_claim": 0,
 					"type": "Earning",
 					"max_benefit_amount": 15000,
+					"depends_on_payment_days": 1,
 				},
 			]
 		)
diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.py b/erpnext/payroll/doctype/salary_structure/salary_structure.py
index fa36b7a..edf17db 100644
--- a/erpnext/payroll/doctype/salary_structure/salary_structure.py
+++ b/erpnext/payroll/doctype/salary_structure/salary_structure.py
@@ -253,6 +253,7 @@
 	source_name,
 	target_doc=None,
 	employee=None,
+	posting_date=None,
 	as_print=False,
 	print_format=None,
 	for_preview=0,
@@ -269,6 +270,9 @@
 			target.designation = employee_details.designation
 			target.department = employee_details.department
 
+			if posting_date:
+				target.posting_date = posting_date
+
 		target.run_method("process_salary_structure", for_preview=for_preview)
 
 	doc = get_mapped_doc(
diff --git a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py
index e9b5ed2..8cc2ea3 100644
--- a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py
+++ b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py
@@ -169,9 +169,6 @@
 	payroll_period=None,
 	include_flexi_benefits=False,
 ):
-	if test_tax:
-		frappe.db.sql("""delete from `tabSalary Structure` where name=%s""", (salary_structure))
-
 	if frappe.db.exists("Salary Structure", salary_structure):
 		frappe.db.delete("Salary Structure", salary_structure)
 
@@ -230,9 +227,12 @@
 	company=None,
 	currency=erpnext.get_default_currency(),
 	payroll_period=None,
+	base=None,
+	allow_duplicate=False,
 ):
-
-	if frappe.db.exists("Salary Structure Assignment", {"employee": employee}):
+	if not allow_duplicate and frappe.db.exists(
+		"Salary Structure Assignment", {"employee": employee}
+	):
 		frappe.db.sql("""delete from `tabSalary Structure Assignment` where employee=%s""", (employee))
 
 	if not payroll_period:
@@ -245,7 +245,7 @@
 
 	salary_structure_assignment = frappe.new_doc("Salary Structure Assignment")
 	salary_structure_assignment.employee = employee
-	salary_structure_assignment.base = 50000
+	salary_structure_assignment.base = base or 50000
 	salary_structure_assignment.variable = 5000
 
 	if not from_date:
diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js
index 58eb891..a5b7699 100644
--- a/erpnext/public/js/controllers/buying.js
+++ b/erpnext/public/js/controllers/buying.js
@@ -74,6 +74,7 @@
 		me.frm.set_query('supplier_address', erpnext.queries.address_query);
 
 		me.frm.set_query('billing_address', erpnext.queries.company_address_query);
+		erpnext.accounts.dimensions.setup_dimension_filters(me.frm, me.frm.doctype);
 
 		if(this.frm.fields_dict.supplier) {
 			this.frm.set_query("supplier", function() {
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index d11205a..de93c82 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -423,7 +423,7 @@
 		item.barcode = null;
 
 
-		if(item.item_code || item.barcode || item.serial_no) {
+		if(item.item_code || item.serial_no) {
 			if(!this.validate_company_and_party()) {
 				this.frm.fields_dict["items"].grid.grid_rows[item.idx - 1].remove();
 			} else {
@@ -463,6 +463,7 @@
 							stock_qty: item.stock_qty,
 							conversion_factor: item.conversion_factor,
 							weight_per_unit: item.weight_per_unit,
+							uom: item.uom,
 							weight_uom: item.weight_uom,
 							manufacturer: item.manufacturer,
 							stock_uom: item.stock_uom,
@@ -526,12 +527,6 @@
 											if(!d[k]) d[k] = v;
 										});
 
-										if (d.__disable_batch_serial_selector) {
-											// reset for future use.
-											d.__disable_batch_serial_selector = false;
-											return;
-										}
-
 										if (d.has_batch_no && d.has_serial_no) {
 											d.batch_no = undefined;
 										}
diff --git a/erpnext/public/js/utils/barcode_scanner.js b/erpnext/public/js/utils/barcode_scanner.js
index eea91ef..a6bff2c 100644
--- a/erpnext/public/js/utils/barcode_scanner.js
+++ b/erpnext/public/js/utils/barcode_scanner.js
@@ -9,6 +9,7 @@
 		this.barcode_field = opts.barcode_field || "barcode";
 		this.serial_no_field = opts.serial_no_field || "serial_no";
 		this.batch_no_field = opts.batch_no_field || "batch_no";
+		this.uom_field = opts.uom_field || "uom";
 		this.qty_field = opts.qty_field || "qty";
 		// field name on row which defines max quantity to be scanned e.g. picklist
 		this.max_qty_field = opts.max_qty_field;
@@ -26,6 +27,7 @@
 		//     bar_code: "123456", // present if barcode was scanned
 		//     batch_no: "LOT12", // present if batch was scanned
 		//     serial_no: "987XYZ", // present if serial no was scanned
+		//     uom: "Kg", // present if barcode UOM is different from default
 		// }
 		this.scan_api = opts.scan_api || "erpnext.stock.utils.scan_barcode";
 	}
@@ -67,9 +69,9 @@
 		return new Promise(resolve => {
 			let cur_grid = this.frm.fields_dict[this.items_table_name].grid;
 
-			const {item_code, barcode, batch_no, serial_no} = data;
+			const {item_code, barcode, batch_no, serial_no, uom} = data;
 
-			let row = this.get_row_to_modify_on_scan(item_code, batch_no);
+			let row = this.get_row_to_modify_on_scan(item_code, batch_no, uom);
 
 			if (!row) {
 				if (this.dont_allow_new_row) {
@@ -90,14 +92,16 @@
 			}
 
 			frappe.run_serially([
-				() => this.set_selector_trigger_flag(row, data),
+				() => this.set_selector_trigger_flag(data),
 				() => this.set_item(row, item_code).then(qty => {
 					this.show_scan_message(row.idx, row.item_code, qty);
 				}),
+				() => this.set_barcode_uom(row, uom),
 				() => this.set_serial_no(row, serial_no),
 				() => this.set_batch_no(row, batch_no),
 				() => this.set_barcode(row, barcode),
 				() => this.clean_up(),
+				() => this.revert_selector_flag(),
 				() => resolve(row)
 			]);
 		});
@@ -105,17 +109,21 @@
 
 	// batch and serial selector is reduandant when all info can be added by scan
 	// this flag on item row is used by transaction.js to avoid triggering selector
-	set_selector_trigger_flag(row, data) {
+	set_selector_trigger_flag(data) {
 		const {batch_no, serial_no, has_batch_no, has_serial_no} = data;
 
 		const require_selecting_batch = has_batch_no && !batch_no;
 		const require_selecting_serial = has_serial_no && !serial_no;
 
 		if (!(require_selecting_batch || require_selecting_serial)) {
-			row.__disable_batch_serial_selector = true;
+			frappe.flags.hide_serial_batch_dialog = true;
 		}
 	}
 
+	revert_selector_flag() {
+		frappe.flags.hide_serial_batch_dialog = false;
+	}
+
 	set_item(row, item_code) {
 		return new Promise(resolve => {
 			const increment = async (value = 1) => {
@@ -149,6 +157,12 @@
 		}
 	}
 
+	async set_barcode_uom(row, uom) {
+		if (uom && frappe.meta.has_field(row.doctype, this.uom_field)) {
+			await frappe.model.set_value(row.doctype, row.name, this.uom_field, uom);
+		}
+	}
+
 	async set_batch_no(row, batch_no) {
 		if (batch_no && frappe.meta.has_field(row.doctype, this.batch_no_field)) {
 			await frappe.model.set_value(row.doctype, row.name, this.batch_no_field, batch_no);
@@ -179,7 +193,7 @@
 		return is_duplicate;
 	}
 
-	get_row_to_modify_on_scan(item_code, batch_no) {
+	get_row_to_modify_on_scan(item_code, batch_no, uom) {
 		let cur_grid = this.frm.fields_dict[this.items_table_name].grid;
 
 		// Check if batch is scanned and table has batch no field
@@ -188,10 +202,12 @@
 
 		const matching_row = (row) => {
 			const item_match = row.item_code == item_code;
-			const batch_match = row.batch_no == batch_no;
+			const batch_match = row[this.batch_no_field] == batch_no;
+			const uom_match = !uom || row[this.uom_field] == uom;
 			const qty_in_limit = flt(row[this.qty_field]) < flt(row[this.max_qty_field]);
 
 			return item_match
+				&& uom_match
 				&& (!is_batch_no_scan || batch_match)
 				&& (!check_max_qty || qty_in_limit)
 		}
diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
index d6210ab..090697b 100644
--- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
+++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
@@ -148,7 +148,6 @@
 			FROM `tabPurchase Invoice` p , `tabPurchase Invoice Item` i
 			WHERE p.docstatus = 1 and p.name = i.parent
 			and p.is_opening = 'No'
-			and p.gst_category != 'Registered Composition'
 			and (i.is_nil_exempt = 1 or i.is_non_gst = 1 or p.gst_category = 'Registered Composition') and
 			month(p.posting_date) = %s and year(p.posting_date) = %s
 			and p.company = %s and p.company_gstin = %s
@@ -245,11 +244,10 @@
 			)
 
 			for d in item_details:
-				if d.item_code not in self.invoice_items.get(d.parent, {}):
-					self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, 0.0)
-					self.invoice_items[d.parent][d.item_code] += d.get("taxable_value", 0) or d.get(
-						"base_net_amount", 0
-					)
+				self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, 0.0)
+				self.invoice_items[d.parent][d.item_code] += d.get("taxable_value", 0) or d.get(
+					"base_net_amount", 0
+				)
 
 				if d.is_nil_exempt and d.item_code not in self.is_nil_exempt:
 					self.is_nil_exempt.append(d.item_code)
@@ -336,7 +334,6 @@
 
 	def set_outward_taxable_supplies(self):
 		inter_state_supply_details = {}
-
 		for inv, items_based_on_rate in self.items_based_on_tax_rate.items():
 			gst_category = self.invoice_detail_map.get(inv, {}).get("gst_category")
 			place_of_supply = (
@@ -362,7 +359,6 @@
 							else:
 								self.report_dict["sup_details"]["osup_det"]["iamt"] += taxable_value * rate / 100
 								self.report_dict["sup_details"]["osup_det"]["txval"] += taxable_value
-
 								if (
 									gst_category in ["Unregistered", "Registered Composition", "UIN Holders"]
 									and self.gst_details.get("gst_state") != place_of_supply.split("-")[1]
diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js
index ef24ce7..580e646 100644
--- a/erpnext/regional/india/e_invoice/einvoice.js
+++ b/erpnext/regional/india/e_invoice/einvoice.js
@@ -150,26 +150,29 @@
 
 			if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) {
 				const action = () => {
-					let message = __('Cancellation of e-way bill is currently not supported.') + ' ';
-					message += '<br><br>';
-					message += __('You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system.');
-
-					const dialog = frappe.msgprint({
-						title: __('Update E-Way Bill Cancelled Status?'),
-						message: message,
-						indicator: 'orange',
-						primary_action: {
-							action: function() {
-								frappe.call({
-									method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill',
-									args: { doctype, docname: name },
-									freeze: true,
-									callback: () => frm.reload_doc() && dialog.hide()
-								});
-							}
+					// This confirm is added to just reduce unnecesory API calls. All required logic is implemented on server side.
+					frappe.confirm(
+						__("Have you cancelled e-way bill on the portal?"),
+						() => {
+							frappe.call({
+								method: "erpnext.regional.india.e_invoice.utils.cancel_eway_bill",
+								args: { doctype, docname: name },
+								freeze: true,
+								callback: () => frm.reload_doc(),
+							});
 						},
-						primary_action_label: __('Yes')
-					});
+						() => {
+							frappe.show_alert(
+								{
+									message: __(
+										"Please cancel e-way bill on the portal first."
+									),
+									indicator: "orange",
+								},
+								5
+							);
+						}
+					);
 				};
 				add_custom_button(__("Cancel E-Way Bill"), action);
 			}
diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py
index e5a1a59..5eb14a5 100644
--- a/erpnext/regional/india/e_invoice/utils.py
+++ b/erpnext/regional/india/e_invoice/utils.py
@@ -55,6 +55,9 @@
 		return False
 
 	invalid_company = not frappe.db.get_value("E Invoice User", {"company": doc.get("company")})
+	invalid_company_gstin = not frappe.db.get_value(
+		"E Invoice User", {"gstin": doc.get("company_gstin")}
+	)
 	invalid_supply_type = doc.get("gst_category") not in [
 		"Registered Regular",
 		"Registered Composition",
@@ -71,6 +74,7 @@
 
 	if (
 		invalid_company
+		or invalid_company_gstin
 		or invalid_supply_type
 		or company_transaction
 		or no_taxes_applied
@@ -803,6 +807,8 @@
 		self.gstin_details_url = self.base_url + "/enriched/ei/api/master/gstin"
 		# cancel_ewaybill_url will only work if user have bought ewb api from adaequare.
 		self.cancel_ewaybill_url = self.base_url + "/enriched/ewb/ewayapi?action=CANEWB"
+		# ewaybill_details_url + ?irn={irn_number} will provide eway bill number and details.
+		self.ewaybill_details_url = self.base_url + "/enriched/ei/api/ewaybill/irn"
 		self.generate_ewaybill_url = self.base_url + "/enriched/ei/api/ewaybill"
 		self.get_qrcode_url = self.base_url + "/enriched/ei/others/qr/image"
 
@@ -1205,23 +1211,22 @@
 			log_error(data)
 			self.raise_error(True)
 
-	def cancel_eway_bill(self, eway_bill, reason, remark):
+	def get_ewb_details(self):
+		"""
+		Get e-Waybill Details by IRN API documentaion for validation is not added yet.
+		https://einv-apisandbox.nic.in/version1.03/get-ewaybill-details-by-irn.html#validations
+		NOTE: if ewaybill Validity period lapsed or scanned by officer enroute (not tested yet) it will still return status as "ACT".
+		"""
 		headers = self.get_headers()
-		data = json.dumps({"ewbNo": eway_bill, "cancelRsnCode": reason, "cancelRmrk": remark}, indent=4)
-		headers["username"] = headers["user_name"]
-		del headers["user_name"]
-		try:
-			res = self.make_request("post", self.cancel_ewaybill_url, headers, data)
-			if res.get("success"):
-				self.invoice.ewaybill = ""
-				self.invoice.eway_bill_cancelled = 1
-				self.invoice.flags.updater_reference = {
-					"doctype": self.invoice.doctype,
-					"docname": self.invoice.name,
-					"label": _("E-Way Bill Cancelled - {}").format(remark),
-				}
-				self.update_invoice()
+		irn = self.invoice.irn
+		if not irn:
+			frappe.throw(_("IRN is mandatory to get E-Waybill Details. Please generate IRN first."))
 
+		try:
+			params = "?irn={irn}".format(irn=irn)
+			res = self.make_request("get", self.ewaybill_details_url + params, headers)
+			if res.get("success"):
+				return res.get("result")
 			else:
 				raise RequestFailed
 
@@ -1230,9 +1235,65 @@
 			self.raise_error(errors=errors)
 
 		except Exception:
-			log_error(data)
+			log_error()
 			self.raise_error(True)
 
+	def update_ewb_details(self, ewb_details=None):
+		# for any reason user chooses to generate eway bill using portal this will allow to update ewaybill details in the invoice.
+		if not self.invoice.irn:
+			frappe.throw(_("IRN is mandatory to update E-Waybill Details. Please generate IRN first."))
+		if not ewb_details:
+			ewb_details = self.get_ewb_details()
+		if ewb_details:
+			self.invoice.ewaybill = ewb_details.get("EwbNo")
+			self.invoice.eway_bill_validity = ewb_details.get("EwbValidTill")
+			self.invoice.eway_bill_cancelled = 0 if ewb_details.get("Status") == "ACT" else 1
+			self.update_invoice()
+
+	def cancel_eway_bill(self):
+		ewb_details = self.get_ewb_details()
+		if ewb_details:
+			ewb_no = str(ewb_details.get("EwbNo"))
+			ewb_status = ewb_details.get("Status")
+			if ewb_status == "CNL":
+				self.invoice.ewaybill = ""
+				self.invoice.eway_bill_cancelled = 1
+				self.invoice.flags.updater_reference = {
+					"doctype": self.invoice.doctype,
+					"docname": self.invoice.name,
+					"label": _("E-Way Bill Cancelled"),
+				}
+				self.update_invoice()
+				frappe.msgprint(
+					_("E-Way Bill Cancelled successfully"),
+					indicator="green",
+					alert=True,
+				)
+			elif ewb_status == "ACT" and self.invoice.ewaybill == ewb_no:
+				msg = _("E-Way Bill {} is still active.").format(bold(ewb_no))
+				msg += "<br><br>"
+				msg += _(
+					"You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system."
+				)
+				frappe.msgprint(msg)
+			elif ewb_status == "ACT" and self.invoice.ewaybill != ewb_no:
+				# if user cancelled the current eway bill and generated new eway bill using portal, then this will update new ewb number in sales invoice.
+				msg = _("E-Way Bill No. {0} doesn't match {1} saved in the invoice.").format(
+					bold(ewb_no), bold(self.invoice.ewaybill)
+				)
+				msg += "<hr/>"
+				msg += _("E-Way Bill No. {} is updated in the invoice.").format(bold(ewb_no))
+				frappe.msgprint(msg)
+				self.update_ewb_details(ewb_details=ewb_details)
+			else:
+				# this block should not be ever called but added incase there is any change in API.
+				msg = _("Unknown E-Way Status Code {}.").format(ewb_status)
+				msg += "<br><br>"
+				msg += _("Please contact your system administrator.")
+				frappe.throw(msg)
+		else:
+			frappe.msgprint(_("E-Way Bill Details not found for this IRN."))
+
 	def sanitize_error_message(self, message):
 		"""
 		On validation errors, response message looks something like this:
@@ -1383,12 +1444,22 @@
 
 @frappe.whitelist()
 def cancel_eway_bill(doctype, docname):
-	# NOTE: cancel_eway_bill api is disabled by Adequare.
-	# gsp_connector = GSPConnector(doctype, docname)
-	# gsp_connector.cancel_eway_bill(eway_bill, reason, remark)
+	# NOTE: cancel_eway_bill api is disabled by NIC for E-invoice so this will only check if eway bill is canceled or not and update accordingly.
+	# https://einv-apisandbox.nic.in/version1.03/cancel-eway-bill.html#
+	gsp_connector = GSPConnector(doctype, docname)
+	gsp_connector.cancel_eway_bill()
 
-	frappe.db.set_value(doctype, docname, "ewaybill", "")
-	frappe.db.set_value(doctype, docname, "eway_bill_cancelled", 1)
+
+@frappe.whitelist()
+def get_ewb_details(doctype, docname):
+	gsp_connector = GSPConnector(doctype, docname)
+	gsp_connector.get_ewb_details()
+
+
+@frappe.whitelist()
+def update_ewb_details(doctype, docname):
+	gsp_connector = GSPConnector(doctype, docname)
+	gsp_connector.update_ewb_details()
 
 
 @frappe.whitelist()
diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py
index 6a7e113..ee48ccb 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -1,14 +1,24 @@
 import json
+import math
 import re
 
 import frappe
 from frappe import _
 from frappe.model.utils import get_fetch_values
-from frappe.utils import cint, cstr, date_diff, flt, getdate, nowdate
+from frappe.utils import (
+	add_days,
+	cint,
+	cstr,
+	date_diff,
+	flt,
+	get_link_to_form,
+	getdate,
+	month_diff,
+)
 
 from erpnext.controllers.accounts_controller import get_taxes_and_charges
 from erpnext.controllers.taxes_and_totals import get_itemised_tax, get_itemised_taxable_amount
-from erpnext.hr.utils import get_salary_assignment
+from erpnext.hr.utils import get_salary_assignments
 from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip
 from erpnext.regional.india import number_state_mapping, state_numbers, states
 
@@ -359,45 +369,57 @@
 	basic_component, hra_component = frappe.db.get_value(
 		"Company", doc.company, ["basic_component", "hra_component"]
 	)
+
 	if not (basic_component and hra_component):
-		frappe.throw(_("Please mention Basic and HRA component in Company"))
-	annual_exemption, monthly_exemption, hra_amount = 0, 0, 0
+		frappe.throw(
+			_("Please set Basic and HRA component in Company {0}").format(
+				get_link_to_form("Company", doc.company)
+			)
+		)
+
+	annual_exemption = monthly_exemption = hra_amount = basic_amount = 0
+
 	if hra_component and basic_component:
-		assignment = get_salary_assignment(doc.employee, nowdate())
-		if assignment:
-			hra_component_exists = frappe.db.exists(
-				"Salary Detail",
-				{
-					"parent": assignment.salary_structure,
-					"salary_component": hra_component,
-					"parentfield": "earnings",
-					"parenttype": "Salary Structure",
-				},
-			)
+		assignments = get_salary_assignments(doc.employee, doc.payroll_period)
 
-			if hra_component_exists:
-				basic_amount, hra_amount = get_component_amt_from_salary_slip(
-					doc.employee, assignment.salary_structure, basic_component, hra_component
-				)
-				if hra_amount:
-					if doc.monthly_house_rent:
-						annual_exemption = calculate_hra_exemption(
-							assignment.salary_structure,
-							basic_amount,
-							hra_amount,
-							doc.monthly_house_rent,
-							doc.rented_in_metro_city,
-						)
-						if annual_exemption > 0:
-							monthly_exemption = annual_exemption / 12
-						else:
-							annual_exemption = 0
-
-		elif doc.docstatus == 1:
+		if not assignments and doc.docstatus == 1:
 			frappe.throw(
-				_("Salary Structure must be submitted before submission of Tax Ememption Declaration")
+				_("Salary Structure must be submitted before submission of {0}").format(doc.doctype)
 			)
 
+		assignment_dates = [assignment.from_date for assignment in assignments]
+
+		for idx, assignment in enumerate(assignments):
+			if has_hra_component(assignment.salary_structure, hra_component):
+				basic_salary_amt, hra_salary_amt = get_component_amt_from_salary_slip(
+					doc.employee,
+					assignment.salary_structure,
+					basic_component,
+					hra_component,
+					assignment.from_date,
+				)
+				to_date = get_end_date_for_assignment(assignment_dates, idx, doc.payroll_period)
+
+				frequency = frappe.get_value(
+					"Salary Structure", assignment.salary_structure, "payroll_frequency"
+				)
+				basic_amount += get_component_pay(frequency, basic_salary_amt, assignment.from_date, to_date)
+				hra_amount += get_component_pay(frequency, hra_salary_amt, assignment.from_date, to_date)
+
+		if hra_amount:
+			if doc.monthly_house_rent:
+				annual_exemption = calculate_hra_exemption(
+					assignment.salary_structure,
+					basic_amount,
+					hra_amount,
+					doc.monthly_house_rent,
+					doc.rented_in_metro_city,
+				)
+				if annual_exemption > 0:
+					monthly_exemption = annual_exemption / 12
+				else:
+					annual_exemption = 0
+
 	return frappe._dict(
 		{
 			"hra_amount": hra_amount,
@@ -407,10 +429,44 @@
 	)
 
 
-def get_component_amt_from_salary_slip(employee, salary_structure, basic_component, hra_component):
-	salary_slip = make_salary_slip(
-		salary_structure, employee=employee, for_preview=1, ignore_permissions=True
+def has_hra_component(salary_structure, hra_component):
+	return frappe.db.exists(
+		"Salary Detail",
+		{
+			"parent": salary_structure,
+			"salary_component": hra_component,
+			"parentfield": "earnings",
+			"parenttype": "Salary Structure",
+		},
 	)
+
+
+def get_end_date_for_assignment(assignment_dates, idx, payroll_period):
+	end_date = None
+
+	try:
+		end_date = assignment_dates[idx + 1]
+		end_date = add_days(end_date, -1)
+	except IndexError:
+		pass
+
+	if not end_date:
+		end_date = frappe.db.get_value("Payroll Period", payroll_period, "end_date")
+
+	return end_date
+
+
+def get_component_amt_from_salary_slip(
+	employee, salary_structure, basic_component, hra_component, from_date
+):
+	salary_slip = make_salary_slip(
+		salary_structure,
+		employee=employee,
+		for_preview=1,
+		ignore_permissions=True,
+		posting_date=from_date,
+	)
+
 	basic_amt, hra_amt = 0, 0
 	for earning in salary_slip.earnings:
 		if earning.salary_component == basic_component:
@@ -423,36 +479,37 @@
 
 
 def calculate_hra_exemption(
-	salary_structure, basic, monthly_hra, monthly_house_rent, rented_in_metro_city
+	salary_structure, annual_basic, annual_hra, monthly_house_rent, rented_in_metro_city
 ):
 	# TODO make this configurable
 	exemptions = []
-	frequency = frappe.get_value("Salary Structure", salary_structure, "payroll_frequency")
 	# case 1: The actual amount allotted by the employer as the HRA.
-	exemptions.append(get_annual_component_pay(frequency, monthly_hra))
-
-	actual_annual_rent = monthly_house_rent * 12
-	annual_basic = get_annual_component_pay(frequency, basic)
+	exemptions.append(annual_hra)
 
 	# case 2: Actual rent paid less 10% of the basic salary.
+	actual_annual_rent = monthly_house_rent * 12
 	exemptions.append(flt(actual_annual_rent) - flt(annual_basic * 0.1))
+
 	# case 3: 50% of the basic salary, if the employee is staying in a metro city (40% for a non-metro city).
 	exemptions.append(annual_basic * 0.5 if rented_in_metro_city else annual_basic * 0.4)
+
 	# return minimum of 3 cases
 	return min(exemptions)
 
 
-def get_annual_component_pay(frequency, amount):
+def get_component_pay(frequency, amount, from_date, to_date):
+	days = date_diff(to_date, from_date) + 1
+
 	if frequency == "Daily":
-		return amount * 365
+		return amount * days
 	elif frequency == "Weekly":
-		return amount * 52
+		return amount * math.floor(days / 7)
 	elif frequency == "Fortnightly":
-		return amount * 26
+		return amount * math.floor(days / 14)
 	elif frequency == "Monthly":
-		return amount * 12
+		return amount * month_diff(to_date, from_date)
 	elif frequency == "Bimonthly":
-		return amount * 6
+		return amount * (month_diff(to_date, from_date) / 2)
 
 
 def validate_house_rent_dates(doc):
diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py
index 0bdbe56..6cbc12c 100644
--- a/erpnext/regional/report/gstr_1/gstr_1.py
+++ b/erpnext/regional/report/gstr_1/gstr_1.py
@@ -1155,8 +1155,11 @@
 		.inner_join(links)
 		.on(address.name == links.parent)
 		.select(address.gstin)
+		.distinct()
 		.where(links.link_doctype == "Company")
 		.where(links.link_name == company)
+		.where(address.gstin.isnotnull())
+		.where(address.gstin != "")
 		.run(as_dict=1)
 	)
 
diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py
index 8889a5f..35e0b0d 100644
--- a/erpnext/selling/doctype/customer/customer.py
+++ b/erpnext/selling/doctype/customer/customer.py
@@ -141,6 +141,9 @@
 				)
 
 	def validate_internal_customer(self):
+		if not self.is_internal_customer:
+			self.represents_company = ""
+
 		internal_customer = frappe.db.get_value(
 			"Customer",
 			{
diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json
index ff921c7..74c5c07 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.json
+++ b/erpnext/selling/doctype/sales_order/sales_order.json
@@ -1359,6 +1359,8 @@
    "width": "50%"
   },
   {
+   "fetch_from": "sales_partner.commission_rate",
+   "fetch_if_empty": 1,
    "fieldname": "commission_rate",
    "fieldtype": "Float",
    "hide_days": 1,
@@ -1547,7 +1549,7 @@
  "idx": 105,
  "is_submittable": 1,
  "links": [],
- "modified": "2022-04-26 14:38:18.350207",
+ "modified": "2022-06-10 03:52:22.212953",
  "modified_by": "Administrator",
  "module": "Selling",
  "name": "Sales Order",
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index acae37f..96308f0 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -783,6 +783,7 @@
 
 	def test_auto_insert_price(self):
 		make_item("_Test Item for Auto Price List", {"is_stock_item": 0})
+		make_item("_Test Item for Auto Price List with Discount Percentage", {"is_stock_item": 0})
 		frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 1)
 
 		item_price = frappe.db.get_value(
@@ -804,6 +805,25 @@
 			100,
 		)
 
+		make_sales_order(
+			item_code="_Test Item for Auto Price List with Discount Percentage",
+			selling_price_list="_Test Price List",
+			price_list_rate=200,
+			discount_percentage=20,
+		)
+
+		self.assertEqual(
+			frappe.db.get_value(
+				"Item Price",
+				{
+					"price_list": "_Test Price List",
+					"item_code": "_Test Item for Auto Price List with Discount Percentage",
+				},
+				"price_list_rate",
+			),
+			200,
+		)
+
 		# do not update price list
 		frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 0)
 
@@ -1659,7 +1679,9 @@
 				"warehouse": args.warehouse,
 				"qty": args.qty or 10,
 				"uom": args.uom or None,
-				"rate": args.rate or 100,
+				"price_list_rate": args.price_list_rate or None,
+				"discount_percentage": args.discount_percentage or None,
+				"rate": args.rate or (None if args.price_list_rate else 100),
 				"against_blanket_order": args.against_blanket_order,
 			},
 		)
diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json
index 005e24c..2abb169 100644
--- a/erpnext/selling/doctype/selling_settings/selling_settings.json
+++ b/erpnext/selling/doctype/selling_settings/selling_settings.json
@@ -179,7 +179,7 @@
  "index_web_pages_for_search": 1,
  "issingle": 1,
  "links": [],
- "modified": "2022-04-14 16:01:29.405642",
+ "modified": "2022-05-31 19:39:48.398738",
  "modified_by": "Administrator",
  "module": "Selling",
  "name": "Selling Settings",
@@ -193,6 +193,15 @@
    "role": "System Manager",
    "share": 1,
    "write": 1
+  },
+  {
+   "create": 1,
+   "email": 1,
+   "print": 1,
+   "read": 1,
+   "role": "Sales Manager",
+   "share": 1,
+   "write": 1
   }
  ],
  "sort_field": "modified",
diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.py b/erpnext/selling/doctype/selling_settings/selling_settings.py
index 6c09894..d977807 100644
--- a/erpnext/selling/doctype/selling_settings/selling_settings.py
+++ b/erpnext/selling/doctype/selling_settings/selling_settings.py
@@ -27,7 +27,7 @@
 		]:
 			frappe.db.set_default(key, self.get(key, ""))
 
-		from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series
+		from erpnext.utilities.naming import set_by_naming_series
 
 		set_by_naming_series(
 			"Customer",
diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
index dcfb10a..cc61594 100644
--- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
+++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
@@ -1,11 +1,13 @@
 # Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
 # For license information, please see license.txt
 
-
 import copy
+from collections import OrderedDict
 
 import frappe
-from frappe import _
+from frappe import _, qb
+from frappe.query_builder import CustomFunction
+from frappe.query_builder.functions import Max
 from frappe.utils import date_diff, flt, getdate
 
 
@@ -18,11 +20,12 @@
 	columns = get_columns(filters)
 	conditions = get_conditions(filters)
 	data = get_data(conditions, filters)
+	so_elapsed_time = get_so_elapsed_time(data)
 
 	if not data:
 		return [], [], None, []
 
-	data, chart_data = prepare_data(data, filters)
+	data, chart_data = prepare_data(data, so_elapsed_time, filters)
 
 	return columns, data, None, chart_data
 
@@ -65,7 +68,6 @@
 			IF(so.status in ('Completed','To Bill'), 0, (SELECT delay_days)) as delay,
 			soi.qty, soi.delivered_qty,
 			(soi.qty - soi.delivered_qty) AS pending_qty,
-			IF((SELECT pending_qty) = 0, (TO_SECONDS(Max(dn.posting_date))-TO_SECONDS(so.transaction_date)), 0) as time_taken_to_deliver,
 			IFNULL(SUM(sii.qty), 0) as billed_qty,
 			soi.base_amount as amount,
 			(soi.delivered_qty * soi.base_rate) as delivered_qty_amount,
@@ -76,13 +78,9 @@
 			soi.description as description
 		FROM
 			`tabSales Order` so,
-			(`tabSales Order Item` soi
+			`tabSales Order Item` soi
 		LEFT JOIN `tabSales Invoice Item` sii
-			ON sii.so_detail = soi.name and sii.docstatus = 1)
-		LEFT JOIN `tabDelivery Note Item` dni
-			on dni.so_detail = soi.name
-		LEFT JOIN `tabDelivery Note` dn
-			on dni.parent = dn.name and dn.docstatus = 1
+			ON sii.so_detail = soi.name and sii.docstatus = 1
 		WHERE
 			soi.parent = so.name
 			and so.status not in ('Stopped', 'Closed', 'On Hold')
@@ -100,7 +98,48 @@
 	return data
 
 
-def prepare_data(data, filters):
+def get_so_elapsed_time(data):
+	"""
+	query SO's elapsed time till latest delivery note
+	"""
+	so_elapsed_time = OrderedDict()
+	if data:
+		sales_orders = [x.sales_order for x in data]
+
+		so = qb.DocType("Sales Order")
+		soi = qb.DocType("Sales Order Item")
+		dn = qb.DocType("Delivery Note")
+		dni = qb.DocType("Delivery Note Item")
+
+		to_seconds = CustomFunction("TO_SECONDS", ["date"])
+
+		query = (
+			qb.from_(so)
+			.inner_join(soi)
+			.on(soi.parent == so.name)
+			.left_join(dni)
+			.on(dni.so_detail == soi.name)
+			.left_join(dn)
+			.on(dni.parent == dn.name)
+			.select(
+				so.name.as_("sales_order"),
+				soi.item_code.as_("so_item_code"),
+				(to_seconds(Max(dn.posting_date)) - to_seconds(so.transaction_date)).as_("elapsed_seconds"),
+			)
+			.where((so.name.isin(sales_orders)) & (dn.docstatus == 1))
+			.orderby(so.name, soi.name)
+			.groupby(soi.name)
+		)
+		dn_elapsed_time = query.run(as_dict=True)
+
+		for e in dn_elapsed_time:
+			key = (e.sales_order, e.so_item_code)
+			so_elapsed_time[key] = e.elapsed_seconds
+
+	return so_elapsed_time
+
+
+def prepare_data(data, so_elapsed_time, filters):
 	completed, pending = 0, 0
 
 	if filters.get("group_by_so"):
@@ -115,6 +154,13 @@
 		row["qty_to_bill"] = flt(row["qty"]) - flt(row["billed_qty"])
 
 		row["delay"] = 0 if row["delay"] and row["delay"] < 0 else row["delay"]
+
+		row["time_taken_to_deliver"] = (
+			so_elapsed_time.get((row.sales_order, row.item_code))
+			if row["status"] in ("To Bill", "Completed")
+			else 0
+		)
+
 		if filters.get("group_by_so"):
 			so_name = row["sales_order"]
 
diff --git a/erpnext/selling/report/sales_order_analysis/test_sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/test_sales_order_analysis.py
index 25cbb73..241f435 100644
--- a/erpnext/selling/report/sales_order_analysis/test_sales_order_analysis.py
+++ b/erpnext/selling/report/sales_order_analysis/test_sales_order_analysis.py
@@ -11,7 +11,7 @@
 
 
 class TestSalesOrderAnalysis(FrappeTestCase):
-	def create_sales_order(self, transaction_date):
+	def create_sales_order(self, transaction_date, do_not_save=False, do_not_submit=False):
 		item = create_item(item_code="_Test Excavator", is_stock_item=0)
 		so = make_sales_order(
 			transaction_date=transaction_date,
@@ -24,25 +24,31 @@
 		so.taxes_and_charges = ""
 		so.taxes = ""
 		so.items[0].delivery_date = add_days(transaction_date, 15)
-		so.save()
-		so.submit()
+		if not do_not_save:
+			so.save()
+			if not do_not_submit:
+				so.submit()
 		return item, so
 
-	def create_sales_invoice(self, so):
+	def create_sales_invoice(self, so, do_not_save=False, do_not_submit=False):
 		sinv = make_sales_invoice(so.name)
 		sinv.posting_date = so.transaction_date
 		sinv.taxes_and_charges = ""
 		sinv.taxes = ""
-		sinv.insert()
-		sinv.submit()
+		if not do_not_save:
+			sinv.save()
+			if not do_not_submit:
+				sinv.submit()
 		return sinv
 
-	def create_delivery_note(self, so):
+	def create_delivery_note(self, so, do_not_save=False, do_not_submit=False):
 		dn = make_delivery_note(so.name)
 		dn.set_posting_time = True
 		dn.posting_date = add_days(so.transaction_date, 1)
-		dn.save()
-		dn.submit()
+		if not do_not_save:
+			dn.save()
+			if not do_not_submit:
+				dn.submit()
 		return dn
 
 	def test_01_so_to_deliver_and_bill(self):
@@ -164,3 +170,85 @@
 		)
 		# SO's from first 4 test cases should be in output
 		self.assertEqual(len(data), 4)
+
+	def test_06_so_pending_delivery_with_multiple_delivery_notes(self):
+		transaction_date = "2021-06-01"
+		item, so = self.create_sales_order(transaction_date)
+
+		# bill 2 items
+		sinv1 = self.create_sales_invoice(so, do_not_save=True)
+		sinv1.items[0].qty = 2
+		sinv1 = sinv1.save().submit()
+		# deliver 2 items
+		dn1 = self.create_delivery_note(so, do_not_save=True)
+		dn1.items[0].qty = 2
+		dn1 = dn1.save().submit()
+
+		# bill 2 items
+		sinv2 = self.create_sales_invoice(so, do_not_save=True)
+		sinv2.items[0].qty = 2
+		sinv2 = sinv2.save().submit()
+		# deliver 1 item
+		dn2 = self.create_delivery_note(so, do_not_save=True)
+		dn2.items[0].qty = 1
+		dn2 = dn2.save().submit()
+
+		columns, data, message, chart = execute(
+			{
+				"company": "_Test Company",
+				"from_date": "2021-06-01",
+				"to_date": "2021-06-30",
+				"sales_order": [so.name],
+			}
+		)
+		expected_value = {
+			"status": "To Deliver and Bill",
+			"sales_order": so.name,
+			"delay_days": frappe.utils.date_diff(frappe.utils.datetime.date.today(), so.delivery_date),
+			"qty": 10,
+			"delivered_qty": 3,
+			"pending_qty": 7,
+			"qty_to_bill": 6,
+			"billed_qty": 4,
+			"time_taken_to_deliver": 0,
+		}
+		self.assertEqual(len(data), 1)
+		for key, val in expected_value.items():
+			with self.subTest(key=key, val=val):
+				self.assertEqual(data[0][key], val)
+
+	def test_07_so_delivered_with_multiple_delivery_notes(self):
+		transaction_date = "2021-06-01"
+		item, so = self.create_sales_order(transaction_date)
+
+		dn1 = self.create_delivery_note(so, do_not_save=True)
+		dn1.items[0].qty = 5
+		dn1 = dn1.save().submit()
+
+		dn2 = self.create_delivery_note(so, do_not_save=True)
+		dn2.items[0].qty = 5
+		dn2 = dn2.save().submit()
+
+		columns, data, message, chart = execute(
+			{
+				"company": "_Test Company",
+				"from_date": "2021-06-01",
+				"to_date": "2021-06-30",
+				"sales_order": [so.name],
+			}
+		)
+		expected_value = {
+			"status": "To Bill",
+			"sales_order": so.name,
+			"delay_days": frappe.utils.date_diff(frappe.utils.datetime.date.today(), so.delivery_date),
+			"qty": 10,
+			"delivered_qty": 10,
+			"pending_qty": 0,
+			"qty_to_bill": 10,
+			"billed_qty": 0,
+			"time_taken_to_deliver": 86400,
+		}
+		self.assertEqual(len(data), 1)
+		for key, val in expected_value.items():
+			with self.subTest(key=key, val=val):
+				self.assertEqual(data[0][key], val)
diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js
index 0954de4..8ff01f5 100644
--- a/erpnext/selling/sales_common.js
+++ b/erpnext/selling/sales_common.js
@@ -12,8 +12,6 @@
 erpnext.selling.SellingController = class SellingController extends erpnext.TransactionController {
 	setup() {
 		super.setup();
-		this.frm.add_fetch("sales_partner", "commission_rate", "commission_rate");
-		this.frm.add_fetch("sales_person", "commission_rate", "commission_rate");
 	}
 
 	onload() {
@@ -43,6 +41,7 @@
 		me.frm.set_query('shipping_address_name', erpnext.queries.address_query);
 		me.frm.set_query('dispatch_address_name', erpnext.queries.dispatch_address_query);
 
+		erpnext.accounts.dimensions.setup_dimension_filters(me.frm, me.frm.doctype);
 
 		if(this.frm.fields_dict.selling_price_list) {
 			this.frm.set_query("selling_price_list", function() {
@@ -513,4 +512,4 @@
 
 		dialog.show();
 	}
-})
\ No newline at end of file
+})
diff --git a/erpnext/setup/doctype/authorization_control/authorization_control.py b/erpnext/setup/doctype/authorization_control/authorization_control.py
index 309658d..cfe3d62 100644
--- a/erpnext/setup/doctype/authorization_control/authorization_control.py
+++ b/erpnext/setup/doctype/authorization_control/authorization_control.py
@@ -135,8 +135,8 @@
 			price_list_rate, base_rate = 0, 0
 			for d in doc_obj.get("items"):
 				if d.base_rate:
-					price_list_rate += flt(d.base_price_list_rate) or flt(d.base_rate)
-					base_rate += flt(d.base_rate)
+					price_list_rate += (flt(d.base_price_list_rate) or flt(d.base_rate)) * flt(d.qty)
+					base_rate += flt(d.base_rate) * flt(d.qty)
 			if doc_obj.get("discount_amount"):
 				base_rate -= flt(doc_obj.discount_amount)
 
diff --git a/erpnext/setup/doctype/naming_series/README.md b/erpnext/setup/doctype/naming_series/README.md
deleted file mode 100644
index 5a9b8ca..0000000
--- a/erpnext/setup/doctype/naming_series/README.md
+++ /dev/null
@@ -1 +0,0 @@
-Tool to set numbering (naming) series for various DocTypes.
\ No newline at end of file
diff --git a/erpnext/setup/doctype/naming_series/__init__.py b/erpnext/setup/doctype/naming_series/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/setup/doctype/naming_series/__init__.py
+++ /dev/null
diff --git a/erpnext/setup/doctype/naming_series/naming_series.js b/erpnext/setup/doctype/naming_series/naming_series.js
deleted file mode 100644
index 0fb72ab..0000000
--- a/erpnext/setup/doctype/naming_series/naming_series.js
+++ /dev/null
@@ -1,88 +0,0 @@
-// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-
-frappe.ui.form.on("Naming Series", {
-	onload: function(frm) {
-		frm.events.get_doc_and_prefix(frm);
-	},
-
-	refresh: function(frm) {
-		frm.disable_save();
-	},
-
-	get_doc_and_prefix: function(frm) {
-		frappe.call({
-			method: "get_transactions",
-			doc: frm.doc,
-			callback: function(r) {
-				frm.set_df_property("select_doc_for_series", "options", r.message.transactions);
-				frm.set_df_property("prefix", "options", r.message.prefixes);
-			}
-		});
-	},
-
-	select_doc_for_series: function(frm) {
-		frm.set_value("user_must_always_select", 0);
-		frappe.call({
-			method: "get_options",
-			doc: frm.doc,
-			callback: function(r) {
-				frm.set_value("set_options", r.message);
-				if(r.message && r.message.split('\n')[0]=='')
-					frm.set_value('user_must_always_select', 1);
-				frm.refresh();
-			}
-		});
-	},
-
-	prefix: function(frm) {
-		frappe.call({
-			method: "get_current",
-			doc: frm.doc,
-			callback: function(r) {
-				frm.refresh_field("current_value");
-			}
-		});
-	},
-
-	update: function(frm) {
-		frappe.call({
-			method: "update_series",
-			doc: frm.doc,
-			callback: function(r) {
-				frm.events.get_doc_and_prefix(frm);
-			}
-		});
-	},
-
-	naming_series_to_check(frm) {
-		frappe.call({
-			method: "preview_series",
-			doc: frm.doc,
-			callback: function(r) {
-				if (!r.exc) {
-					frm.set_value("preview", r.message);
-				} else {
-					frm.set_value("preview", __("Failed to generate preview of series"));
-				}
-			}
-		});
-	},
-
-	add_series(frm) {
-		const series = frm.doc.naming_series_to_check;
-
-		if (!series) {
-			frappe.show_alert(__("Please type a valid series."));
-			return;
-		}
-
-		if (!frm.doc.set_options.includes(series)) {
-			const current_series = frm.doc.set_options;
-			frm.set_value("set_options", `${current_series}\n${series}`);
-		} else {
-			frappe.show_alert(__("Series already added to transaction."));
-		}
-	},
-});
diff --git a/erpnext/setup/doctype/naming_series/naming_series.json b/erpnext/setup/doctype/naming_series/naming_series.json
deleted file mode 100644
index c65a6f0..0000000
--- a/erpnext/setup/doctype/naming_series/naming_series.json
+++ /dev/null
@@ -1,132 +0,0 @@
-{
- "actions": [],
- "creation": "2022-05-26 03:12:49.087648",
- "description": "Set prefix for numbering series on your transactions",
- "doctype": "DocType",
- "engine": "InnoDB",
- "field_order": [
-  "setup_series",
-  "select_doc_for_series",
-  "help_html",
-  "naming_series_to_check",
-  "preview",
-  "add_series",
-  "set_options",
-  "user_must_always_select",
-  "update",
-  "column_break_13",
-  "update_series",
-  "prefix",
-  "current_value",
-  "update_series_start"
- ],
- "fields": [
-  {
-   "description": "Set prefix for numbering series on your transactions",
-   "fieldname": "setup_series",
-   "fieldtype": "Section Break",
-   "label": "Setup Series"
-  },
-  {
-   "fieldname": "select_doc_for_series",
-   "fieldtype": "Select",
-   "label": "Select Transaction"
-  },
-  {
-   "depends_on": "select_doc_for_series",
-   "fieldname": "help_html",
-   "fieldtype": "HTML",
-   "label": "Help HTML",
-   "options": "<div class=\"well\">\n    Edit list of Series in the box below. Rules:\n    <ul>\n        <li>Each Series Prefix on a new line.</li>\n        <li>Allowed special characters are \"/\" and \"-\"</li>\n        <li>\n            Optionally, set the number of digits in the series using dot (.)\n            followed by hashes (#). For example, \".####\" means that the series\n            will have four digits. Default is five digits.\n        </li>\n        <li>\n            You can also use variables in the series name by putting them\n            between (.) dots\n            <br>\n            Support Variables:\n            <ul>\n                <li><code>.YYYY.</code> - Year in 4 digits</li>\n                <li><code>.YY.</code> - Year in 2 digits</li>\n                <li><code>.MM.</code> - Month</li>\n                <li><code>.DD.</code> - Day of month</li>\n                <li><code>.WW.</code> - Week of the year</li>\n                <li><code>.FY.</code> - Fiscal Year</li>\n                <li>\n                    <code>.{fieldname}.</code> - fieldname on the document e.g.\n                    <code>branch</code>\n                </li>\n            </ul>\n        </li>\n    </ul>\n    Examples:\n    <ul>\n        <li>INV-</li>\n        <li>INV-10-</li>\n        <li>INVK-</li>\n        <li>INV-.YYYY.-.{branch}.-.MM.-.####</li>\n    </ul>\n</div>\n<br>\n"
-  },
-  {
-   "depends_on": "select_doc_for_series",
-   "fieldname": "set_options",
-   "fieldtype": "Text",
-   "label": "Series List for this Transaction"
-  },
-  {
-   "default": "0",
-   "depends_on": "select_doc_for_series",
-   "description": "Check this if you want to force the user to select a series before saving. There will be no default if you check this.",
-   "fieldname": "user_must_always_select",
-   "fieldtype": "Check",
-   "label": "User must always select"
-  },
-  {
-   "depends_on": "select_doc_for_series",
-   "fieldname": "update",
-   "fieldtype": "Button",
-   "label": "Update"
-  },
-  {
-   "description": "Change the starting / current sequence number of an existing series.",
-   "fieldname": "update_series",
-   "fieldtype": "Section Break",
-   "label": "Update Series"
-  },
-  {
-   "fieldname": "prefix",
-   "fieldtype": "Select",
-   "label": "Prefix"
-  },
-  {
-   "description": "This is the number of the last created transaction with this prefix",
-   "fieldname": "current_value",
-   "fieldtype": "Int",
-   "label": "Current Value"
-  },
-  {
-   "fieldname": "update_series_start",
-   "fieldtype": "Button",
-   "label": "Update Series Number",
-   "options": "update_series_start"
-  },
-  {
-   "fieldname": "naming_series_to_check",
-   "fieldtype": "Data",
-   "label": "Try a naming Series"
-  },
-  {
-   "default": " ",
-   "fieldname": "preview",
-   "fieldtype": "Text",
-   "label": "Preview of generated names",
-   "read_only": 1
-  },
-  {
-   "fieldname": "column_break_13",
-   "fieldtype": "Column Break"
-  },
-  {
-   "fieldname": "add_series",
-   "fieldtype": "Button",
-   "label": "Add this Series"
-  }
- ],
- "hide_toolbar": 1,
- "icon": "fa fa-sort-by-order",
- "idx": 1,
- "issingle": 1,
- "links": [],
- "modified": "2022-05-26 06:06:42.109504",
- "modified_by": "Administrator",
- "module": "Setup",
- "name": "Naming Series",
- "owner": "Administrator",
- "permissions": [
-  {
-   "create": 1,
-   "email": 1,
-   "print": 1,
-   "read": 1,
-   "role": "System Manager",
-   "share": 1,
-   "write": 1
-  }
- ],
- "read_only": 1,
- "sort_field": "modified",
- "sort_order": "DESC",
- "states": []
-}
\ No newline at end of file
diff --git a/erpnext/setup/doctype/naming_series/naming_series.py b/erpnext/setup/doctype/naming_series/naming_series.py
deleted file mode 100644
index eafc264..0000000
--- a/erpnext/setup/doctype/naming_series/naming_series.py
+++ /dev/null
@@ -1,303 +0,0 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# License: GNU General Public License v3. See license.txt
-
-
-import frappe
-from frappe import _, msgprint, throw
-from frappe.core.doctype.doctype.doctype import validate_series
-from frappe.model.document import Document
-from frappe.model.naming import make_autoname, parse_naming_series
-from frappe.permissions import get_doctypes_with_read
-from frappe.utils import cint, cstr
-
-
-class NamingSeriesNotSetError(frappe.ValidationError):
-	pass
-
-
-class NamingSeries(Document):
-	@frappe.whitelist()
-	def get_transactions(self, arg=None):
-		doctypes = list(
-			set(
-				frappe.db.sql_list(
-					"""select parent
-				from `tabDocField` df where fieldname='naming_series'"""
-				)
-				+ frappe.db.sql_list(
-					"""select dt from `tabCustom Field`
-				where fieldname='naming_series'"""
-				)
-			)
-		)
-
-		doctypes = list(set(get_doctypes_with_read()).intersection(set(doctypes)))
-		prefixes = ""
-		for d in doctypes:
-			options = ""
-			try:
-				options = self.get_options(d)
-			except frappe.DoesNotExistError:
-				frappe.msgprint(_("Unable to find DocType {0}").format(d))
-				# frappe.pass_does_not_exist_error()
-				continue
-
-			if options:
-				prefixes = prefixes + "\n" + options
-		prefixes.replace("\n\n", "\n")
-		prefixes = prefixes.split("\n")
-
-		custom_prefixes = frappe.get_all(
-			"DocType",
-			fields=["autoname"],
-			filters={
-				"name": ("not in", doctypes),
-				"autoname": ("like", "%.#%"),
-				"module": ("not in", ["Core"]),
-			},
-		)
-		if custom_prefixes:
-			prefixes = prefixes + [d.autoname.rsplit(".", 1)[0] for d in custom_prefixes]
-
-		prefixes = "\n".join(sorted(prefixes))
-
-		return {"transactions": "\n".join([""] + sorted(doctypes)), "prefixes": prefixes}
-
-	def scrub_options_list(self, ol):
-		options = list(filter(lambda x: x, [cstr(n).strip() for n in ol]))
-		return options
-
-	@frappe.whitelist()
-	def update_series(self, arg=None):
-		"""update series list"""
-		self.validate_series_set()
-		self.check_duplicate()
-		series_list = self.set_options.split("\n")
-
-		# set in doctype
-		self.set_series_for(self.select_doc_for_series, series_list)
-
-		# create series
-		map(self.insert_series, [d.split(".")[0] for d in series_list if d.strip()])
-
-		msgprint(_("Series Updated"))
-
-		return self.get_transactions()
-
-	def validate_series_set(self):
-		if self.select_doc_for_series and not self.set_options:
-			frappe.throw(_("Please set the series to be used."))
-
-	def set_series_for(self, doctype, ol):
-		options = self.scrub_options_list(ol)
-
-		# validate names
-		for i in options:
-			self.validate_series_name(i)
-
-		if options and self.user_must_always_select:
-			options = [""] + options
-
-		default = options[0] if options else ""
-
-		# update in property setter
-		prop_dict = {"options": "\n".join(options), "default": default}
-
-		for prop in prop_dict:
-			ps_exists = frappe.db.get_value(
-				"Property Setter", {"field_name": "naming_series", "doc_type": doctype, "property": prop}
-			)
-
-			if ps_exists:
-				ps = frappe.get_doc("Property Setter", ps_exists)
-				ps.value = prop_dict[prop]
-				ps.save()
-			else:
-				ps = frappe.get_doc(
-					{
-						"doctype": "Property Setter",
-						"doctype_or_field": "DocField",
-						"doc_type": doctype,
-						"field_name": "naming_series",
-						"property": prop,
-						"value": prop_dict[prop],
-						"property_type": "Text",
-						"__islocal": 1,
-					}
-				)
-				ps.save()
-
-		self.set_options = "\n".join(options)
-
-		frappe.clear_cache(doctype=doctype)
-
-	def check_duplicate(self):
-		parent = list(
-			set(
-				frappe.db.sql_list(
-					"""select dt.name
-				from `tabDocField` df, `tabDocType` dt
-				where dt.name = df.parent and df.fieldname='naming_series' and dt.name != %s""",
-					self.select_doc_for_series,
-				)
-				+ frappe.db.sql_list(
-					"""select dt.name
-				from `tabCustom Field` df, `tabDocType` dt
-				where dt.name = df.dt and df.fieldname='naming_series' and dt.name != %s""",
-					self.select_doc_for_series,
-				)
-			)
-		)
-		sr = [[frappe.get_meta(p).get_field("naming_series").options, p] for p in parent]
-		dt = frappe.get_doc("DocType", self.select_doc_for_series)
-		options = self.scrub_options_list(self.set_options.split("\n"))
-		for series in options:
-			validate_series(dt, series)
-			for i in sr:
-				if i[0]:
-					existing_series = [d.split(".")[0] for d in i[0].split("\n")]
-					if series.split(".")[0] in existing_series:
-						frappe.throw(_("Series {0} already used in {1}").format(series, i[1]))
-
-	def validate_series_name(self, n):
-		import re
-
-		if not re.match(r"^[\w\- \/.#{}]+$", n, re.UNICODE):
-			throw(
-				_('Special Characters except "-", "#", ".", "/", "{" and "}" not allowed in naming series')
-			)
-
-	@frappe.whitelist()
-	def get_options(self, arg=None):
-		if frappe.get_meta(arg or self.select_doc_for_series).get_field("naming_series"):
-			return frappe.get_meta(arg or self.select_doc_for_series).get_field("naming_series").options
-
-	@frappe.whitelist()
-	def get_current(self, arg=None):
-		"""get series current"""
-		if self.prefix:
-			prefix = self.parse_naming_series()
-			self.current_value = frappe.db.get_value("Series", prefix, "current", order_by="name")
-
-	def insert_series(self, series):
-		"""insert series if missing"""
-		if frappe.db.get_value("Series", series, "name", order_by="name") == None:
-			frappe.db.sql("insert into tabSeries (name, current) values (%s, 0)", (series))
-
-	@frappe.whitelist()
-	def update_series_start(self):
-		if self.prefix:
-			prefix = self.parse_naming_series()
-			self.insert_series(prefix)
-			frappe.db.sql(
-				"update `tabSeries` set current = %s where name = %s", (cint(self.current_value), prefix)
-			)
-			msgprint(_("Series Updated Successfully"))
-		else:
-			msgprint(_("Please select prefix first"))
-
-	def parse_naming_series(self):
-		parts = self.prefix.split(".")
-
-		# Remove ### from the end of series
-		if parts[-1] == "#" * len(parts[-1]):
-			del parts[-1]
-
-		prefix = parse_naming_series(parts)
-		return prefix
-
-	@frappe.whitelist()
-	def preview_series(self) -> str:
-		"""Preview what the naming series will generate."""
-
-		generated_names = []
-		series = self.naming_series_to_check
-		if not series:
-			return ""
-
-		try:
-			doc = self._fetch_last_doc_if_available()
-			for _count in range(3):
-				generated_names.append(make_autoname(series, doc=doc))
-		except Exception as e:
-			if frappe.message_log:
-				frappe.message_log.pop()
-			return _("Failed to generate names from the series") + f"\n{str(e)}"
-
-		# Explcitly rollback in case any changes were made to series table.
-		frappe.db.rollback()  # nosemgrep
-		return "\n".join(generated_names)
-
-	def _fetch_last_doc_if_available(self):
-		"""Fetch last doc for evaluating naming series with fields."""
-		try:
-			return frappe.get_last_doc(self.select_doc_for_series)
-		except Exception:
-			return None
-
-
-def set_by_naming_series(
-	doctype, fieldname, naming_series, hide_name_field=True, make_mandatory=1
-):
-	from frappe.custom.doctype.property_setter.property_setter import make_property_setter
-
-	if naming_series:
-		make_property_setter(
-			doctype, "naming_series", "hidden", 0, "Check", validate_fields_for_doctype=False
-		)
-		make_property_setter(
-			doctype, "naming_series", "reqd", make_mandatory, "Check", validate_fields_for_doctype=False
-		)
-
-		# set values for mandatory
-		try:
-			frappe.db.sql(
-				"""update `tab{doctype}` set naming_series={s} where
-				ifnull(naming_series, '')=''""".format(
-					doctype=doctype, s="%s"
-				),
-				get_default_naming_series(doctype),
-			)
-		except NamingSeriesNotSetError:
-			pass
-
-		if hide_name_field:
-			make_property_setter(doctype, fieldname, "reqd", 0, "Check", validate_fields_for_doctype=False)
-			make_property_setter(
-				doctype, fieldname, "hidden", 1, "Check", validate_fields_for_doctype=False
-			)
-	else:
-		make_property_setter(
-			doctype, "naming_series", "reqd", 0, "Check", validate_fields_for_doctype=False
-		)
-		make_property_setter(
-			doctype, "naming_series", "hidden", 1, "Check", validate_fields_for_doctype=False
-		)
-
-		if hide_name_field:
-			make_property_setter(
-				doctype, fieldname, "hidden", 0, "Check", validate_fields_for_doctype=False
-			)
-			make_property_setter(doctype, fieldname, "reqd", 1, "Check", validate_fields_for_doctype=False)
-
-			# set values for mandatory
-			frappe.db.sql(
-				"""update `tab{doctype}` set `{fieldname}`=`name` where
-				ifnull({fieldname}, '')=''""".format(
-					doctype=doctype, fieldname=fieldname
-				)
-			)
-
-
-def get_default_naming_series(doctype):
-	naming_series = frappe.get_meta(doctype).get_field("naming_series").options or ""
-	naming_series = naming_series.split("\n")
-	out = naming_series[0] or (naming_series[1] if len(naming_series) > 1 else None)
-
-	if not out:
-		frappe.throw(
-			_("Please set Naming Series for {0} via Setup > Settings > Naming Series").format(doctype),
-			NamingSeriesNotSetError,
-		)
-	else:
-		return out
diff --git a/erpnext/setup/doctype/naming_series/test_naming_series.py b/erpnext/setup/doctype/naming_series/test_naming_series.py
deleted file mode 100644
index fce663e..0000000
--- a/erpnext/setup/doctype/naming_series/test_naming_series.py
+++ /dev/null
@@ -1,35 +0,0 @@
-# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-import frappe
-from frappe.tests.utils import FrappeTestCase
-
-from erpnext.setup.doctype.naming_series.naming_series import NamingSeries
-
-
-class TestNamingSeries(FrappeTestCase):
-	def setUp(self):
-		self.ns: NamingSeries = frappe.get_doc("Naming Series")
-
-	def tearDown(self):
-		frappe.db.rollback()
-
-	def test_naming_preview(self):
-		self.ns.select_doc_for_series = "Sales Invoice"
-
-		self.ns.naming_series_to_check = "AXBZ.####"
-		serieses = self.ns.preview_series().split("\n")
-		self.assertEqual(["AXBZ0001", "AXBZ0002", "AXBZ0003"], serieses)
-
-		self.ns.naming_series_to_check = "AXBZ-.{currency}.-"
-		serieses = self.ns.preview_series().split("\n")
-
-	def test_get_transactions(self):
-
-		naming_info = self.ns.get_transactions()
-		self.assertIn("Sales Invoice", naming_info["transactions"])
-
-		existing_naming_series = frappe.get_meta("Sales Invoice").get_field("naming_series").options
-
-		for series in existing_naming_series.split("\n"):
-			self.assertIn(series, naming_info["prefixes"])
diff --git a/erpnext/setup/module_onboarding/home/home.json b/erpnext/setup/module_onboarding/home/home.json
index 1b2dbc6..f02fc45 100644
--- a/erpnext/setup/module_onboarding/home/home.json
+++ b/erpnext/setup/module_onboarding/home/home.json
@@ -25,7 +25,7 @@
  "documentation_url": "https://docs.erpnext.com/docs/v13/user/manual/en/setting-up/company-setup",
  "idx": 0,
  "is_complete": 0,
- "modified": "2021-12-15 14:23:52.460913",
+ "modified": "2022-06-07 14:31:00.575193",
  "modified_by": "Administrator",
  "module": "Setup",
  "name": "Home",
diff --git a/erpnext/setup/onboarding_step/data_import/data_import.json b/erpnext/setup/onboarding_step/data_import/data_import.json
index 48741dc..4999a36 100644
--- a/erpnext/setup/onboarding_step/data_import/data_import.json
+++ b/erpnext/setup/onboarding_step/data_import/data_import.json
@@ -2,14 +2,14 @@
  "action": "Watch Video",
  "action_label": "Learn more about data migration",
  "creation": "2021-05-19 05:29:16.809610",
- "description": "# Import Data from Spreadsheet\n\nIn ERPNext, you can easily migrate your historical data using spreadsheets. You can use it for migrating not just masters (like Customer, Supplier, Items), but also for transactions like (outstanding invoices, opening stock and accounting entries, etc). If you are migrating from [Tally](https://tallysolutions.com/) or [Quickbooks](https://quickbooks.intuit.com/in/), we got special migration tools for you.",
+ "description": "# Import Data from Spreadsheet\n\nIn ERPNext, you can easily migrate your historical data using spreadsheets. You can use it for migrating not just masters (like Customer, Supplier, Items), but also for transactions like (outstanding invoices, opening stock and accounting entries, etc).",
  "docstatus": 0,
  "doctype": "Onboarding Step",
  "idx": 0,
  "is_complete": 0,
  "is_single": 0,
  "is_skipped": 0,
- "modified": "2021-12-15 13:10:57.346422",
+ "modified": "2022-06-07 14:28:51.390813",
  "modified_by": "Administrator",
  "name": "Data import",
  "owner": "Administrator",
diff --git a/erpnext/setup/onboarding_step/navigation_help/navigation_help.json b/erpnext/setup/onboarding_step/navigation_help/navigation_help.json
index 388853d..cf07968 100644
--- a/erpnext/setup/onboarding_step/navigation_help/navigation_help.json
+++ b/erpnext/setup/onboarding_step/navigation_help/navigation_help.json
@@ -9,7 +9,7 @@
  "is_complete": 0,
  "is_single": 0,
  "is_skipped": 0,
- "modified": "2021-12-15 14:20:55.441678",
+ "modified": "2022-06-07 14:28:00.901082",
  "modified_by": "Administrator",
  "name": "Navigation Help",
  "owner": "Administrator",
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js
index 706ca36..ea3cf19 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.js
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.js
@@ -77,8 +77,6 @@
 			}
 		});
 
-		erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
-
 		frm.set_df_property('packed_items', 'cannot_add_rows', true);
 		frm.set_df_property('packed_items', 'cannot_delete_rows', true);
 	},
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json
index e3222bc..f9e9349 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.json
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.json
@@ -1192,6 +1192,8 @@
    "width": "50%"
   },
   {
+   "fetch_from": "sales_partner.commission_rate",
+   "fetch_if_empty": 1,
    "fieldname": "commission_rate",
    "fieldtype": "Float",
    "label": "Commission Rate (%)",
@@ -1334,7 +1336,7 @@
  "idx": 146,
  "is_submittable": 1,
  "links": [],
- "modified": "2022-04-26 14:48:08.781837",
+ "modified": "2022-06-10 03:52:04.197415",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Delivery Note",
diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json
index 4f3e842..2f6d4fb 100644
--- a/erpnext/stock/doctype/item/item.json
+++ b/erpnext/stock/doctype/item/item.json
@@ -11,7 +11,7 @@
  "editable_grid": 1,
  "engine": "InnoDB",
  "field_order": [
-  "name_and_description_section",
+  "details",
   "naming_series",
   "item_code",
   "variant_of",
@@ -35,11 +35,11 @@
   "over_billing_allowance",
   "image",
   "section_break_11",
-  "brand",
   "description",
-  "sb_barcodes",
-  "barcodes",
+  "brand",
+  "dashboard_tab",
   "inventory_section",
+  "inventory_settings_section",
   "shelf_life_in_days",
   "end_of_life",
   "default_material_request_type",
@@ -49,6 +49,8 @@
   "weight_per_unit",
   "weight_uom",
   "allow_negative_stock",
+  "sb_barcodes",
+  "barcodes",
   "reorder_section",
   "reorder_levels",
   "unit_of_measure_conversion",
@@ -67,13 +69,13 @@
   "has_variants",
   "variant_based_on",
   "attributes",
-  "defaults",
+  "accounting",
   "item_defaults",
-  "purchase_details",
-  "is_purchase_item",
+  "purchasing_tab",
   "purchase_uom",
   "min_order_qty",
   "safety_stock",
+  "is_purchase_item",
   "purchase_details_cb",
   "lead_time_days",
   "last_purchase_rate",
@@ -83,33 +85,31 @@
   "delivered_by_supplier",
   "column_break2",
   "supplier_items",
+  "deferred_expense_section",
+  "enable_deferred_expense",
+  "deferred_expense_account",
+  "no_of_months_exp",
   "foreign_trade_details",
   "country_of_origin",
   "column_break_59",
   "customs_tariff_number",
   "sales_details",
   "sales_uom",
-  "is_sales_item",
   "grant_commission",
+  "is_sales_item",
   "column_break3",
   "max_discount",
   "deferred_revenue",
-  "deferred_revenue_account",
   "enable_deferred_revenue",
-  "column_break_85",
+  "deferred_revenue_account",
   "no_of_months",
-  "deferred_expense_section",
-  "deferred_expense_account",
-  "enable_deferred_expense",
-  "column_break_88",
-  "no_of_months_exp",
   "customer_details",
   "customer_items",
   "item_tax_section_break",
   "taxes",
-  "inspection_criteria",
-  "quality_inspection_template",
+  "quality_tab",
   "inspection_required_before_purchase",
+  "quality_inspection_template",
   "inspection_required_before_delivery",
   "manufacturing",
   "default_bom",
@@ -118,18 +118,11 @@
   "customer_code",
   "default_item_manufacturer",
   "default_manufacturer_part_no",
-  "more_information_section",
   "published_in_website",
   "total_projected_qty"
  ],
  "fields": [
   {
-   "fieldname": "name_and_description_section",
-   "fieldtype": "Section Break",
-   "oldfieldtype": "Section Break",
-   "options": "fa fa-flag"
-  },
-  {
    "fieldname": "naming_series",
    "fieldtype": "Select",
    "label": "Series",
@@ -315,7 +308,7 @@
    "collapsible_depends_on": "is_stock_item",
    "depends_on": "is_stock_item",
    "fieldname": "inventory_section",
-   "fieldtype": "Section Break",
+   "fieldtype": "Tab Break",
    "label": "Inventory",
    "oldfieldtype": "Section Break",
    "options": "fa fa-truck"
@@ -515,30 +508,16 @@
    "options": "Item Variant Attribute"
   },
   {
-   "depends_on": "eval:!doc.is_fixed_asset",
-   "fieldname": "defaults",
-   "fieldtype": "Section Break",
-   "label": "Sales, Purchase, Accounting Defaults"
-  },
-  {
    "fieldname": "item_defaults",
    "fieldtype": "Table",
    "label": "Item Defaults",
    "options": "Item Default"
   },
   {
-   "collapsible": 1,
-   "fieldname": "purchase_details",
-   "fieldtype": "Section Break",
-   "label": "Purchase, Replenishment Details",
-   "oldfieldtype": "Section Break",
-   "options": "fa fa-shopping-cart"
-  },
-  {
    "default": "1",
    "fieldname": "is_purchase_item",
    "fieldtype": "Check",
-   "label": "Is Purchase Item"
+   "label": "Allow Purchase"
   },
   {
    "fieldname": "purchase_uom",
@@ -646,8 +625,8 @@
   {
    "collapsible": 1,
    "fieldname": "sales_details",
-   "fieldtype": "Section Break",
-   "label": "Sales Details",
+   "fieldtype": "Tab Break",
+   "label": "Sales",
    "oldfieldtype": "Section Break",
    "options": "fa fa-tag"
   },
@@ -661,7 +640,7 @@
    "default": "1",
    "fieldname": "is_sales_item",
    "fieldtype": "Check",
-   "label": "Is Sales Item"
+   "label": "Allow Sales"
   },
   {
    "fieldname": "column_break3",
@@ -697,10 +676,6 @@
    "label": "Enable Deferred Revenue"
   },
   {
-   "fieldname": "column_break_85",
-   "fieldtype": "Column Break"
-  },
-  {
    "depends_on": "enable_deferred_revenue",
    "fieldname": "no_of_months",
    "fieldtype": "Int",
@@ -727,10 +702,6 @@
    "label": "Enable Deferred Expense"
   },
   {
-   "fieldname": "column_break_88",
-   "fieldtype": "Column Break"
-  },
-  {
    "depends_on": "enable_deferred_expense",
    "fieldname": "no_of_months_exp",
    "fieldtype": "Int",
@@ -753,8 +724,8 @@
    "collapsible": 1,
    "collapsible_depends_on": "taxes",
    "fieldname": "item_tax_section_break",
-   "fieldtype": "Section Break",
-   "label": "Item Tax",
+   "fieldtype": "Tab Break",
+   "label": "Tax",
    "oldfieldtype": "Section Break",
    "options": "fa fa-money"
   },
@@ -768,15 +739,6 @@
    "options": "Item Tax"
   },
   {
-   "collapsible": 1,
-   "depends_on": "eval:!doc.is_fixed_asset",
-   "fieldname": "inspection_criteria",
-   "fieldtype": "Section Break",
-   "label": "Inspection Criteria",
-   "oldfieldtype": "Section Break",
-   "options": "fa fa-search"
-  },
-  {
    "default": "0",
    "fieldname": "inspection_required_before_purchase",
    "fieldtype": "Check",
@@ -801,7 +763,7 @@
    "collapsible": 1,
    "depends_on": "is_stock_item",
    "fieldname": "manufacturing",
-   "fieldtype": "Section Break",
+   "fieldtype": "Tab Break",
    "label": "Manufacturing",
    "oldfieldtype": "Section Break",
    "options": "fa fa-cogs"
@@ -881,12 +843,6 @@
    "read_only": 1
   },
   {
-   "collapsible": 1,
-   "fieldname": "more_information_section",
-   "fieldtype": "Section Break",
-   "label": "More Information"
-  },
-  {
    "default": "0",
    "depends_on": "published_in_website",
    "fieldname": "published_in_website",
@@ -912,6 +868,40 @@
    "fieldname": "allow_negative_stock",
    "fieldtype": "Check",
    "label": "Allow Negative Stock"
+  },
+  {
+   "fieldname": "inventory_settings_section",
+   "fieldtype": "Section Break",
+   "label": "Inventory Settings"
+  },
+  {
+   "fieldname": "purchasing_tab",
+   "fieldtype": "Tab Break",
+   "label": "Purchasing"
+  },
+  {
+   "fieldname": "quality_tab",
+   "fieldtype": "Tab Break",
+   "label": "Quality"
+  },
+  {
+   "fieldname": "details",
+   "fieldtype": "Tab Break",
+   "label": "Details",
+   "oldfieldtype": "Section Break",
+   "options": "fa fa-flag"
+  },
+  {
+   "fieldname": "dashboard_tab",
+   "fieldtype": "Tab Break",
+   "label": "Dashboard",
+   "show_dashboard": 1
+  },
+  {
+   "depends_on": "eval:!doc.is_fixed_asset",
+   "fieldname": "accounting",
+   "fieldtype": "Tab Break",
+   "label": "Accounting"
   }
  ],
  "icon": "fa fa-tag",
@@ -919,7 +909,7 @@
  "image_field": "image",
  "index_web_pages_for_search": 1,
  "links": [],
- "modified": "2022-04-28 04:52:10.272256",
+ "modified": "2022-06-08 11:35:20.094546",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Item",
diff --git a/erpnext/stock/doctype/item_barcode/item_barcode.json b/erpnext/stock/doctype/item_barcode/item_barcode.json
index eef70c9..56832f3 100644
--- a/erpnext/stock/doctype/item_barcode/item_barcode.json
+++ b/erpnext/stock/doctype/item_barcode/item_barcode.json
@@ -6,7 +6,8 @@
  "engine": "InnoDB",
  "field_order": [
   "barcode",
-  "barcode_type"
+  "barcode_type",
+  "uom"
  ],
  "fields": [
   {
@@ -24,11 +25,18 @@
    "in_list_view": 1,
    "label": "Barcode Type",
    "options": "\nEAN\nUPC-A"
+  },
+  {
+   "fieldname": "uom",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "label": "UOM",
+   "options": "UOM"
   }
  ],
  "istable": 1,
  "links": [],
- "modified": "2022-04-01 05:54:27.314030",
+ "modified": "2022-06-01 06:24:33.969534",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Item Barcode",
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
index 51ec598..754404b 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
@@ -46,8 +46,6 @@
 		erpnext.queries.setup_queries(frm, "Warehouse", function() {
 			return erpnext.queries.warehouse(frm.doc);
 		});
-
-		erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
 	},
 
 	refresh: function(frm) {
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
index 983b62a..923ceb3 100755
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
@@ -108,8 +108,6 @@
   "terms_section_break",
   "tc_name",
   "terms",
-  "bill_no",
-  "bill_date",
   "more_info",
   "status",
   "amended_from",
@@ -868,24 +866,6 @@
    "oldfieldtype": "Text Editor"
   },
   {
-   "fieldname": "bill_no",
-   "fieldtype": "Data",
-   "hidden": 1,
-   "label": "Bill No",
-   "oldfieldname": "bill_no",
-   "oldfieldtype": "Data",
-   "print_hide": 1
-  },
-  {
-   "fieldname": "bill_date",
-   "fieldtype": "Date",
-   "hidden": 1,
-   "label": "Bill Date",
-   "oldfieldname": "bill_date",
-   "oldfieldtype": "Date",
-   "print_hide": 1
-  },
-  {
    "collapsible": 1,
    "fieldname": "more_info",
    "fieldtype": "Section Break",
@@ -1168,7 +1148,7 @@
  "idx": 261,
  "is_submittable": 1,
  "links": [],
- "modified": "2022-04-26 13:41:32.625197",
+ "modified": "2022-05-27 15:59:18.550583",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Purchase Receipt",
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py
index c5c0cef..41a3b89 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py
@@ -2,11 +2,37 @@
 # See license.txt
 
 
+from typing import TYPE_CHECKING, Optional, overload
+
 import frappe
 from frappe.utils import cint, flt
 
 import erpnext
 
+if TYPE_CHECKING:
+	from erpnext.stock.doctype.stock_entry.stock_entry import StockEntry
+
+
+@overload
+def make_stock_entry(
+	*,
+	item_code: str,
+	qty: float,
+	company: Optional[str] = None,
+	from_warehouse: Optional[str] = None,
+	to_warehouse: Optional[str] = None,
+	rate: Optional[float] = None,
+	serial_no: Optional[str] = None,
+	batch_no: Optional[str] = None,
+	posting_date: Optional[str] = None,
+	posting_time: Optional[str] = None,
+	purpose: Optional[str] = None,
+	do_not_save: bool = False,
+	do_not_submit: bool = False,
+	inspection_required: bool = False,
+) -> "StockEntry":
+	...
+
 
 @frappe.whitelist()
 def make_stock_entry(**args):
diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
index eb1e0fc..55a213c 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
@@ -24,9 +24,10 @@
 	create_stock_reconciliation,
 )
 from erpnext.stock.stock_ledger import get_previous_sle
+from erpnext.stock.tests.test_utils import StockTestMixin
 
 
-class TestStockLedgerEntry(FrappeTestCase):
+class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
 	def setUp(self):
 		items = create_items()
 		reset("Stock Entry")
@@ -541,30 +542,6 @@
 				"Incorrect 'Incoming Rate' values fetched for DN items",
 			)
 
-	def assertSLEs(self, doc, expected_sles, sle_filters=None):
-		"""Compare sorted SLEs, useful for vouchers that create multiple SLEs for same line"""
-
-		filters = {"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled": 0}
-		if sle_filters:
-			filters.update(sle_filters)
-		sles = frappe.get_all(
-			"Stock Ledger Entry",
-			fields=["*"],
-			filters=filters,
-			order_by="timestamp(posting_date, posting_time), creation",
-		)
-
-		for exp_sle, act_sle in zip(expected_sles, sles):
-			for k, v in exp_sle.items():
-				act_value = act_sle[k]
-				if k == "stock_queue":
-					act_value = json.loads(act_value)
-					if act_value and act_value[0][0] == 0:
-						# ignore empty fifo bins
-						continue
-
-				self.assertEqual(v, act_value, msg=f"{k} doesn't match \n{exp_sle}\n{act_sle}")
-
 	def test_batchwise_item_valuation_stock_reco(self):
 		item, warehouses, batches = setup_item_valuation_test()
 		state = {"stock_value": 0.0, "qty": 0.0}
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index 9088eb8..191c03f 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -10,7 +10,7 @@
 from frappe.utils import add_days, cstr, flt, nowdate, nowtime, random_string
 
 from erpnext.accounts.utils import get_stock_and_account_balance
-from erpnext.stock.doctype.item.test_item import create_item, make_item
+from erpnext.stock.doctype.item.test_item import create_item
 from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
 from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
@@ -19,10 +19,11 @@
 )
 from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
 from erpnext.stock.stock_ledger import get_previous_sle, update_entries_after
+from erpnext.stock.tests.test_utils import StockTestMixin
 from erpnext.stock.utils import get_incoming_rate, get_stock_value_on, get_valuation_method
 
 
-class TestStockReconciliation(FrappeTestCase):
+class TestStockReconciliation(FrappeTestCase, StockTestMixin):
 	@classmethod
 	def setUpClass(cls):
 		create_batch_or_serial_no_items()
@@ -40,7 +41,7 @@
 		self._test_reco_sle_gle("Moving Average")
 
 	def _test_reco_sle_gle(self, valuation_method):
-		item_code = make_item(properties={"valuation_method": valuation_method}).name
+		item_code = self.make_item(properties={"valuation_method": valuation_method}).name
 
 		se1, se2, se3 = insert_existing_sle(warehouse="Stores - TCP1", item_code=item_code)
 		company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
@@ -392,7 +393,7 @@
 		SR4		| Reco	|	0	|	6	(posting date: today-1) [backdated]
 		PR3		| PR	|	1	|	7	(posting date: today) # can't post future PR
 		"""
-		item_code = make_item().name
+		item_code = self.make_item().name
 		warehouse = "_Test Warehouse - _TC"
 
 		frappe.flags.dont_execute_stock_reposts = True
@@ -458,7 +459,7 @@
 		from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
 		from erpnext.stock.stock_ledger import NegativeStockError
 
-		item_code = make_item().name
+		item_code = self.make_item().name
 		warehouse = "_Test Warehouse - _TC"
 
 		pr1 = make_purchase_receipt(
@@ -506,7 +507,7 @@
 		from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
 		from erpnext.stock.stock_ledger import NegativeStockError
 
-		item_code = make_item().name
+		item_code = self.make_item().name
 		warehouse = "_Test Warehouse - _TC"
 
 		sr = create_stock_reconciliation(
@@ -549,7 +550,7 @@
 		# repost will make this test useless, qty should update in realtime without reposts
 		frappe.flags.dont_execute_stock_reposts = True
 
-		item_code = make_item().name
+		item_code = self.make_item().name
 		warehouse = "_Test Warehouse - _TC"
 
 		sr = create_stock_reconciliation(
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py
index e592a4b..50807a9 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.py
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.py
@@ -26,7 +26,7 @@
 		]:
 			frappe.db.set_default(key, self.get(key, ""))
 
-		from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series
+		from erpnext.utilities.naming import set_by_naming_series
 
 		set_by_naming_series(
 			"Item",
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index c6241f8..c8d9f54 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -353,6 +353,7 @@
 			"has_batch_no": item.has_batch_no,
 			"batch_no": args.get("batch_no"),
 			"uom": args.uom,
+			"stock_uom": item.stock_uom,
 			"min_order_qty": flt(item.min_order_qty) if args.doctype == "Material Request" else "",
 			"qty": flt(args.qty) or 1.0,
 			"stock_qty": flt(args.qty) or 1.0,
@@ -365,7 +366,7 @@
 			"net_rate": 0.0,
 			"net_amount": 0.0,
 			"discount_percentage": 0.0,
-			"discount_amount": 0.0,
+			"discount_amount": flt(args.discount_amount) or 0.0,
 			"supplier": get_default_supplier(args, item_defaults, item_group_defaults, brand_defaults),
 			"update_stock": args.get("update_stock")
 			if args.get("doctype") in ["Sales Invoice", "Purchase Invoice"]
@@ -823,7 +824,9 @@
 	):
 		if frappe.has_permission("Item Price", "write"):
 			price_list_rate = (
-				args.rate / args.get("conversion_factor") if args.get("conversion_factor") else args.rate
+				(args.rate + args.discount_amount) / args.get("conversion_factor")
+				if args.get("conversion_factor")
+				else (args.rate + args.discount_amount)
 			)
 
 			item_price = frappe.db.get_value(
@@ -849,6 +852,7 @@
 						"item_code": args.item_code,
 						"currency": args.currency,
 						"price_list_rate": price_list_rate,
+						"uom": args.stock_uom,
 					}
 				)
 				item_price.insert()
diff --git a/erpnext/stock/tests/test_utils.py b/erpnext/stock/tests/test_utils.py
index 9ee0c9f..b046dbd 100644
--- a/erpnext/stock/tests/test_utils.py
+++ b/erpnext/stock/tests/test_utils.py
@@ -1,16 +1,67 @@
+import json
+
 import frappe
 from frappe.tests.utils import FrappeTestCase
 
-from erpnext.stock.doctype.item.test_item import make_item
 from erpnext.stock.utils import scan_barcode
 
 
-class TestStockUtilities(FrappeTestCase):
+class StockTestMixin:
+	"""Mixin to simplfy stock ledger tests, useful for all stock transactions."""
+
+	def make_item(self, item_code=None, properties=None, *args, **kwargs):
+		from erpnext.stock.doctype.item.test_item import make_item
+
+		return make_item(item_code, properties, *args, **kwargs)
+
+	def assertSLEs(self, doc, expected_sles, sle_filters=None):
+		"""Compare sorted SLEs, useful for vouchers that create multiple SLEs for same line"""
+
+		filters = {"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled": 0}
+		if sle_filters:
+			filters.update(sle_filters)
+		sles = frappe.get_all(
+			"Stock Ledger Entry",
+			fields=["*"],
+			filters=filters,
+			order_by="timestamp(posting_date, posting_time), creation",
+		)
+
+		for exp_sle, act_sle in zip(expected_sles, sles):
+			for k, v in exp_sle.items():
+				act_value = act_sle[k]
+				if k == "stock_queue":
+					act_value = json.loads(act_value)
+					if act_value and act_value[0][0] == 0:
+						# ignore empty fifo bins
+						continue
+
+				self.assertEqual(v, act_value, msg=f"{k} doesn't match \n{exp_sle}\n{act_sle}")
+
+	def assertGLEs(self, doc, expected_gles, gle_filters=None, order_by=None):
+		filters = {"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled": 0}
+
+		if gle_filters:
+			filters.update(gle_filters)
+		actual_gles = frappe.get_all(
+			"GL Entry",
+			fields=["*"],
+			filters=filters,
+			order_by=order_by or "posting_date, creation",
+		)
+
+		for exp_gle, act_gle in zip(expected_gles, actual_gles):
+			for k, exp_value in exp_gle.items():
+				act_value = act_gle[k]
+				self.assertEqual(exp_value, act_value, msg=f"{k} doesn't match \n{exp_gle}\n{act_gle}")
+
+
+class TestStockUtilities(FrappeTestCase, StockTestMixin):
 	def test_barcode_scanning(self):
-		simple_item = make_item(properties={"barcodes": [{"barcode": "12399"}]})
+		simple_item = self.make_item(properties={"barcodes": [{"barcode": "12399"}]})
 		self.assertEqual(scan_barcode("12399")["item_code"], simple_item.name)
 
-		batch_item = make_item(properties={"has_batch_no": 1, "create_new_batch": 1})
+		batch_item = self.make_item(properties={"has_batch_no": 1, "create_new_batch": 1})
 		batch = frappe.get_doc(doctype="Batch", item=batch_item.name).insert()
 
 		batch_scan = scan_barcode(batch.name)
@@ -19,7 +70,7 @@
 		self.assertEqual(batch_scan["has_batch_no"], 1)
 		self.assertEqual(batch_scan["has_serial_no"], 0)
 
-		serial_item = make_item(properties={"has_serial_no": 1})
+		serial_item = self.make_item(properties={"has_serial_no": 1})
 		serial = frappe.get_doc(
 			doctype="Serial No", item_code=serial_item.name, serial_no=frappe.generate_hash()
 		).insert()
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index 2120304..6d8fdaa 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -558,7 +558,7 @@
 	barcode_data = frappe.db.get_value(
 		"Item Barcode",
 		{"barcode": search_value},
-		["barcode", "parent as item_code"],
+		["barcode", "parent as item_code", "uom"],
 		as_dict=True,
 	)
 	if barcode_data:
diff --git a/erpnext/translations/ar.csv b/erpnext/translations/ar.csv
index 91a9da9..e62f61a 100644
--- a/erpnext/translations/ar.csv
+++ b/erpnext/translations/ar.csv
@@ -4297,7 +4297,7 @@
 "To allow different rates, disable the {0} checkbox in {1}.",للسماح بمعدلات مختلفة ، قم بتعطيل مربع الاختيار {0} في {1}.,
 Current Odometer Value should be greater than Last Odometer Value {0},يجب أن تكون قيمة عداد المسافات الحالية أكبر من قيمة آخر عداد المسافات {0},
 No additional expenses has been added,لم يتم إضافة مصاريف إضافية,
-Asset{} {assets_link} created for {},الأصل {} {asset_link} الذي تم إنشاؤه لـ {},
+Asset{} {assets_link} created for {},الأصل {} {assets_link} الذي تم إنشاؤه لـ {},
 Row {}: Asset Naming Series is mandatory for the auto creation for item {},الصف {}: سلسلة تسمية الأصول إلزامية للإنشاء التلقائي للعنصر {},
 Assets not created for {0}. You will have to create asset manually.,لم يتم إنشاء الأصول لـ {0}. سيكون عليك إنشاء الأصل يدويًا.,
 {0} {1} has accounting entries in currency {2} for company {3}. Please select a receivable or payable account with currency {2}.,{0} يحتوي {1} على إدخالات محاسبية بالعملة {2} للشركة {3}. الرجاء تحديد حساب مستحق أو دائن بالعملة {2}.,
diff --git a/erpnext/utilities/naming.py b/erpnext/utilities/naming.py
new file mode 100644
index 0000000..52bbade
--- /dev/null
+++ b/erpnext/utilities/naming.py
@@ -0,0 +1,60 @@
+import frappe
+from frappe.model.naming import get_default_naming_series
+
+
+class NamingSeriesNotSetError(frappe.ValidationError):
+	pass
+
+
+def set_by_naming_series(
+	doctype, fieldname, naming_series, hide_name_field=True, make_mandatory=1
+):
+	"""Change a doctype's naming to user naming series"""
+	from frappe.custom.doctype.property_setter.property_setter import make_property_setter
+
+	if naming_series:
+		make_property_setter(
+			doctype, "naming_series", "hidden", 0, "Check", validate_fields_for_doctype=False
+		)
+		make_property_setter(
+			doctype, "naming_series", "reqd", make_mandatory, "Check", validate_fields_for_doctype=False
+		)
+
+		# set values for mandatory
+		try:
+			frappe.db.sql(
+				"""update `tab{doctype}` set naming_series={s} where
+				ifnull(naming_series, '')=''""".format(
+					doctype=doctype, s="%s"
+				),
+				get_default_naming_series(doctype),
+			)
+		except NamingSeriesNotSetError:
+			pass
+
+		if hide_name_field:
+			make_property_setter(doctype, fieldname, "reqd", 0, "Check", validate_fields_for_doctype=False)
+			make_property_setter(
+				doctype, fieldname, "hidden", 1, "Check", validate_fields_for_doctype=False
+			)
+	else:
+		make_property_setter(
+			doctype, "naming_series", "reqd", 0, "Check", validate_fields_for_doctype=False
+		)
+		make_property_setter(
+			doctype, "naming_series", "hidden", 1, "Check", validate_fields_for_doctype=False
+		)
+
+		if hide_name_field:
+			make_property_setter(
+				doctype, fieldname, "hidden", 0, "Check", validate_fields_for_doctype=False
+			)
+			make_property_setter(doctype, fieldname, "reqd", 1, "Check", validate_fields_for_doctype=False)
+
+			# set values for mandatory
+			frappe.db.sql(
+				"""update `tab{doctype}` set `{fieldname}`=`name` where
+				ifnull({fieldname}, '')=''""".format(
+					doctype=doctype, fieldname=fieldname
+				)
+			)
diff --git a/requirements.txt b/requirements.txt
index 85ff515..83e5375 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,7 +3,6 @@
 googlemaps
 plaid-python~=7.2.1
 pycountry~=20.7.3
-PyGithub~=1.55
 python-stdnum~=1.16
 python-youtube~=0.8.0
 taxjar~=1.9.2