Merge branch 'develop' into campaign-module
diff --git a/.eslintrc b/.eslintrc
index e40502a..cb45ce5 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -147,11 +147,14 @@
 		"Chart": true,
 		"Cypress": true,
 		"cy": true,
+		"describe": true,
+		"expect": true,
 		"it": true,
 		"context": true,
 		"before": true,
 		"beforeEach": true,
 		"onScan": true,
-		"extend_cscript": true
+		"extend_cscript": true,
+		"localforage": true,
 	}
 }
diff --git a/.github/helper/install.sh b/.github/helper/install.sh
index f7a7122..455ab86 100644
--- a/.github/helper/install.sh
+++ b/.github/helper/install.sh
@@ -42,6 +42,6 @@
 sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile
 
 bench get-app erpnext "${GITHUB_WORKSPACE}"
-bench start &
+bench start &> bench_run_logs.txt &
 bench --site test_site reinstall --yes
 bench build --app frappe
diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml
new file mode 100644
index 0000000..412a05b
--- /dev/null
+++ b/.github/workflows/ui-tests.yml
@@ -0,0 +1,108 @@
+name: UI
+
+on:
+  pull_request:
+  workflow_dispatch:
+
+jobs:
+  test:
+    runs-on: ubuntu-18.04
+
+    strategy:
+      fail-fast: false
+
+    name: UI Tests (Cypress)
+
+    services:
+      mysql:
+        image: mariadb:10.3
+        env:
+          MYSQL_ALLOW_EMPTY_PASSWORD: YES
+        ports:
+          - 3306:3306
+        options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
+
+    steps:
+      - name: Clone
+        uses: actions/checkout@v2
+
+      - name: Setup Python
+        uses: actions/setup-python@v2
+        with:
+          python-version: 3.7
+
+      - uses: actions/setup-node@v2
+        with:
+          node-version: 14
+          check-latest: true
+
+      - name: Add to Hosts
+        run: |
+          echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
+
+      - name: Cache pip
+        uses: actions/cache@v2
+        with:
+          path: ~/.cache/pip
+          key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
+          restore-keys: |
+            ${{ runner.os }}-pip-
+            ${{ runner.os }}-
+
+      - name: Cache node modules
+        uses: actions/cache@v2
+        env:
+          cache-name: cache-node-modules
+        with:
+          path: ~/.npm
+          key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
+          restore-keys: |
+            ${{ runner.os }}-build-${{ env.cache-name }}-
+            ${{ runner.os }}-build-
+            ${{ runner.os }}-
+
+      - name: Get yarn cache directory path
+        id: yarn-cache-dir-path
+        run: echo "::set-output name=dir::$(yarn cache dir)"
+
+      - uses: actions/cache@v2
+        id: yarn-cache
+        with:
+          path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
+          key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
+          restore-keys: |
+            ${{ runner.os }}-yarn-
+
+      - name: Cache cypress binary
+        uses: actions/cache@v2
+        with:
+          path: ~/.cache
+          key: ${{ runner.os }}-cypress-
+          restore-keys: |
+            ${{ runner.os }}-cypress-
+            ${{ runner.os }}-
+
+      - name: Install
+        run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
+        env:
+          DB: mariadb
+          TYPE: ui
+
+      - name: Site Setup
+        run: cd ~/frappe-bench/ && bench --site test_site execute erpnext.setup.utils.before_tests
+
+      - name: cypress pre-requisites
+        run: cd ~/frappe-bench/apps/frappe && yarn add cypress-file-upload@^5 --no-lockfile
+
+
+      - name: Build Assets
+        run: cd ~/frappe-bench/ && bench build
+
+      - name: UI Tests
+        run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests erpnext --headless
+        env:
+          CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
+
+      - name: Show bench console if tests failed
+        if: ${{ failure() }}
+        run: cat ~/frappe-bench/bench_run_logs.txt
diff --git a/.gitignore b/.gitignore
index 63c51c4..89f5626 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,3 +16,4 @@
 .idea/
 .vscode/
 node_modules/
+.backportrc.json
\ No newline at end of file
diff --git a/cypress.json b/cypress.json
new file mode 100644
index 0000000..02b10d8
--- /dev/null
+++ b/cypress.json
@@ -0,0 +1,11 @@
+{
+  "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
new file mode 100644
index 0000000..da18d93
--- /dev/null
+++ b/cypress/fixtures/example.json
@@ -0,0 +1,5 @@
+{
+  "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_customer.js b/cypress/integration/test_customer.js
new file mode 100644
index 0000000..3d6ed5d
--- /dev/null
+++ b/cypress/integration/test_customer.js
@@ -0,0 +1,13 @@
+
+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
new file mode 100644
index 0000000..fcb7533
--- /dev/null
+++ b/cypress/integration/test_item.js
@@ -0,0 +1,44 @@
+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/plugins/index.js b/cypress/plugins/index.js
new file mode 100644
index 0000000..07d9804
--- /dev/null
+++ b/cypress/plugins/index.js
@@ -0,0 +1,17 @@
+// ***********************************************************
+// 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
new file mode 100644
index 0000000..7ddc80a
--- /dev/null
+++ b/cypress/support/commands.js
@@ -0,0 +1,31 @@
+// ***********************************************
+// 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
new file mode 100644
index 0000000..72070cc
--- /dev/null
+++ b/cypress/support/index.js
@@ -0,0 +1,26 @@
+// ***********************************************************
+// 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
new file mode 100644
index 0000000..d90ebf6
--- /dev/null
+++ b/cypress/tsconfig.json
@@ -0,0 +1,12 @@
+{
+    "compilerOptions": {
+        "allowJs": true,
+        "baseUrl": "../node_modules",
+        "types": [
+            "cypress"
+        ]
+    },
+    "include": [
+        "**/*.*"
+    ]
+}
\ No newline at end of file
diff --git a/erpnext/__init__.py b/erpnext/__init__.py
index 39d9a27..0c96d32 100644
--- a/erpnext/__init__.py
+++ b/erpnext/__init__.py
@@ -5,7 +5,7 @@
 from erpnext.hooks import regional_overrides
 from frappe.utils import getdate
 
-__version__ = '13.5.2'
+__version__ = '13.6.0'
 
 def get_default_company(user=None):
 	'''Get default company for user'''
diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py
index 2f86c6c..335e8a1 100644
--- a/erpnext/accounts/deferred_revenue.py
+++ b/erpnext/accounts/deferred_revenue.py
@@ -301,17 +301,21 @@
 	start_date = add_months(today(), -1)
 	end_date = add_days(today(), -1)
 
-	for record_type in ('Income', 'Expense'):
-		doc = frappe.get_doc(dict(
-			doctype='Process Deferred Accounting',
-			posting_date=posting_date,
-			start_date=start_date,
-			end_date=end_date,
-			type=record_type
-		))
+	companies = frappe.get_all('Company')
 
-		doc.insert()
-		doc.submit()
+	for company in companies:
+		for record_type in ('Income', 'Expense'):
+			doc = frappe.get_doc(dict(
+				doctype='Process Deferred Accounting',
+				company=company.name,
+				posting_date=posting_date,
+				start_date=start_date,
+				end_date=end_date,
+				type=record_type
+			))
+
+			doc.insert()
+			doc.submit()
 
 def make_gl_entries(doc, credit_account, debit_account, against,
 	amount, base_amount, posting_date, project, account_currency, cost_center, item, deferred_process=None):
diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
index 7cd1e77..fac28c9 100644
--- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
+++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
@@ -19,7 +19,7 @@
 
 	def validate(self):
 		if self.document_type in core_doctypes_list + ('Accounting Dimension', 'Project',
-				'Cost Center', 'Accounting Dimension Detail', 'Company') :
+				'Cost Center', 'Accounting Dimension Detail', 'Company', 'Account') :
 
 			msg = _("Not allowed to create accounting dimension for {0}").format(self.document_type)
 			frappe.throw(msg)
diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py
index 5f110e2..ffc9d1c 100644
--- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py
+++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py
@@ -51,7 +51,7 @@
 			self.import_file, self.google_sheets_url
 		)
 
-		if 'Bank Account' not in json.dumps(preview):
+		if 'Bank Account' not in json.dumps(preview['columns']):
 			frappe.throw(_("Please add the Bank Account column"))
 
 		from frappe.core.page.background_jobs.background_jobs import get_info
diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py
index 7c1a171..c6c6892 100644
--- a/erpnext/accounts/doctype/dunning/dunning.py
+++ b/erpnext/accounts/doctype/dunning/dunning.py
@@ -96,7 +96,7 @@
 	grand_total = 0
 	if rate_of_interest:
 		interest_per_year = flt(outstanding_amount) * flt(rate_of_interest) / 100
-		interest_amount = (interest_per_year * cint(overdue_days)) / 365 
+		interest_amount = (interest_per_year * cint(overdue_days)) / 365
 		grand_total = flt(outstanding_amount) + flt(interest_amount) + flt(dunning_fee)
 	dunning_amount = flt(interest_amount) + flt(dunning_fee)
 	return {
diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py
index 948c513..11465b7 100644
--- a/erpnext/accounts/doctype/gl_entry/gl_entry.py
+++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py
@@ -121,8 +121,7 @@
 
 	def check_pl_account(self):
 		if self.is_opening=='Yes' and \
-				frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss" and \
-				self.voucher_type not in ['Purchase Invoice', 'Sales Invoice']:
+				frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss":
 			frappe.throw(_("{0} {1}: 'Profit and Loss' type account {2} not allowed in Opening Entry")
 				.format(self.voucher_type, self.voucher_no, self.account))
 
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index adaf99a..0c21aae 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -1318,9 +1318,9 @@
 
 	return frappe._dict({
 		"due_date": ref_doc.get("due_date"),
-		"total_amount": total_amount,
-		"outstanding_amount": outstanding_amount,
-		"exchange_rate": exchange_rate,
+		"total_amount": flt(total_amount),
+		"outstanding_amount": flt(outstanding_amount),
+		"exchange_rate": flt(exchange_rate),
 		"bill_no": bill_no
 	})
 
diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
index 4641d6b..d1302f5 100644
--- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
@@ -589,9 +589,9 @@
 		party_account_balance = get_balance_on(account=pe.paid_from, cost_center=pe.cost_center)
 
 		self.assertEqual(pe.cost_center, si.cost_center)
-		self.assertEqual(expected_account_balance, account_balance)
-		self.assertEqual(expected_party_balance, party_balance)
-		self.assertEqual(expected_party_account_balance, party_account_balance)
+		self.assertEqual(flt(expected_account_balance), account_balance)
+		self.assertEqual(flt(expected_party_balance), party_balance)
+		self.assertEqual(flt(expected_party_account_balance), party_account_balance)
 
 def create_payment_terms_template():
 
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
index 0b0ee90..500952e 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
@@ -207,10 +207,9 @@
 @frappe.whitelist()
 def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=True):
 	billing_email = frappe.db.sql("""
-		SELECT c.email_id FROM `tabContact` AS c JOIN `tabDynamic Link` AS l ON c.name=l.parent \
-		WHERE l.link_doctype='Customer' and l.link_name='""" + customer_name + """' and \
-		c.is_billing_contact=1 \
-		order by c.creation desc""")
+		SELECT c.email_id FROM `tabContact` AS c JOIN `tabDynamic Link` AS l ON c.name=l.parent
+		WHERE l.link_doctype='Customer' and l.link_name=%s and c.is_billing_contact=1
+		order by c.creation desc""", customer_name)
 
 	if len(billing_email) == 0 or (billing_email[0][0] is None):
 		if billing_and_primary:
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 45d89ad..c1cc092 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -517,6 +517,8 @@
 			if d.category in ('Valuation', 'Total and Valuation')
 			and flt(d.base_tax_amount_after_discount_amount)]
 
+		exchange_rate_map, net_rate_map = get_purchase_document_details(self)
+
 		for item in self.get("items"):
 			if flt(item.base_net_amount):
 				account_currency = get_account_currency(item.expense_account)
@@ -634,6 +636,34 @@
 								"project": item.project or self.project
 							}, account_currency, item=item))
 
+						# check if the exchange rate has changed
+						if item.get('purchase_receipt'):
+							if exchange_rate_map[item.purchase_receipt] and \
+								self.conversion_rate != exchange_rate_map[item.purchase_receipt] and \
+								item.net_rate == net_rate_map[item.pr_detail]:
+
+								discrepancy_caused_by_exchange_rate_difference = (item.qty * item.net_rate) * \
+									(exchange_rate_map[item.purchase_receipt] - self.conversion_rate)
+
+								gl_entries.append(
+									self.get_gl_dict({
+										"account": expense_account,
+										"against": self.supplier,
+										"debit": discrepancy_caused_by_exchange_rate_difference,
+										"cost_center": item.cost_center,
+										"project": item.project or self.project
+									}, account_currency, item=item)
+								)
+								gl_entries.append(
+									self.get_gl_dict({
+										"account": self.get_company_default("exchange_gain_loss_account"),		
+										"against": self.supplier,
+										"credit": discrepancy_caused_by_exchange_rate_difference,
+										"cost_center": item.cost_center,
+										"project": item.project or self.project
+									}, account_currency, item=item)
+								)
+
 					# If asset is bought through this document and not linked to PR
 					if self.update_stock and item.landed_cost_voucher_amount:
 						expenses_included_in_asset_valuation = self.get_company_default("expenses_included_in_asset_valuation")
@@ -1141,6 +1171,36 @@
 		if update:
 			self.db_set('status', self.status, update_modified = update_modified)
 
+# to get details of purchase invoice/receipt from which this doc was created for exchange rate difference handling
+def get_purchase_document_details(doc):
+	if doc.doctype == 'Purchase Invoice':
+		doc_reference = 'purchase_receipt'
+		items_reference = 'pr_detail'
+		parent_doctype = 'Purchase Receipt'
+		child_doctype = 'Purchase Receipt Item'
+	else:
+		doc_reference = 'purchase_invoice'
+		items_reference = 'purchase_invoice_item'
+		parent_doctype = 'Purchase Invoice'
+		child_doctype = 'Purchase Invoice Item'
+
+	purchase_receipts_or_invoices = []
+	items = []
+
+	for item in doc.get('items'):
+		if item.get(doc_reference):
+			purchase_receipts_or_invoices.append(item.get(doc_reference))
+		if item.get(items_reference):
+			items.append(item.get(items_reference))
+	
+	exchange_rate_map = frappe._dict(frappe.get_all(parent_doctype, filters={'name': ('in',
+		purchase_receipts_or_invoices)}, fields=['name', 'conversion_rate'], as_list=1))
+
+	net_rate_map = frappe._dict(frappe.get_all(child_doctype, filters={'name': ('in',
+		items)}, fields=['name', 'net_rate'], as_list=1))
+
+	return exchange_rate_map, net_rate_map
+
 def get_list_context(context=None):
 	from erpnext.controllers.website_list_for_contact import get_list_context
 	list_context = get_list_context(context)
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index 2f5d36c..189260a 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -230,6 +230,27 @@
 			self.assertEqual(expected_values[gle.account][1], gle.debit)
 			self.assertEqual(expected_values[gle.account][2], gle.credit)
 
+	def test_purchase_invoice_with_exchange_rate_difference(self):
+		from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice as create_purchase_invoice
+
+		pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse='Stores - TCP1',
+			currency = "USD", conversion_rate = 70)
+
+		pi = create_purchase_invoice(pr.name)
+		pi.conversion_rate = 80
+
+		pi.insert()
+		pi.submit()		
+
+		# Get exchnage gain and loss account
+		exchange_gain_loss_account = frappe.db.get_value('Company', pi.company, 'exchange_gain_loss_account')
+
+		# fetching the latest GL Entry with exchange gain and loss account account
+		amount = frappe.db.get_value('GL Entry', {'account': exchange_gain_loss_account, 'voucher_no': pi.name}, 'debit')
+		discrepancy_caused_by_exchange_rate_diff = abs(pi.items[0].base_net_amount - pr.items[0].base_net_amount)
+
+		self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount)
+
 	def test_purchase_invoice_change_naming_series(self):
 		pi = frappe.copy_doc(test_records[1])
 		pi.insert()
@@ -1010,21 +1031,21 @@
 		# Check GLE for Purchase Invoice
 		# Zero net effect on final TDS Payable on invoice
 		expected_gle = [
-			['_Test Account Cost for Goods Sold - _TC', 30000, 0],
-			['_Test Account Excise Duty - _TC', 0, 3000],
-			['Creditors - _TC', 0, 27000],
-			['TDS Payable - _TC', 3000, 3000]
+			['_Test Account Cost for Goods Sold - _TC', 30000],
+			['_Test Account Excise Duty - _TC', -3000],
+			['Creditors - _TC', -27000],
+			['TDS Payable - _TC', 0]
 		]
 
-		gl_entries = frappe.db.sql("""select account, debit, credit
+		gl_entries = frappe.db.sql("""select account, sum(debit - credit) as amount
 			from `tabGL Entry`
 			where voucher_type='Purchase Invoice' and voucher_no=%s
+			group by account
 			order by account asc""", (purchase_invoice.name), as_dict=1)
 
 		for i, gle in enumerate(gl_entries):
 			self.assertEqual(expected_gle[i][0], gle.account)
-			self.assertEqual(expected_gle[i][1], gle.debit)
-			self.assertEqual(expected_gle[i][2], gle.credit)
+			self.assertEqual(expected_gle[i][1], gle.amount)
 
 def update_tax_witholding_category(company, account, date):
 	from erpnext.accounts.utils import get_fiscal_year
diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
index 29f7247..b39022d 100644
--- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
+++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
@@ -854,7 +854,7 @@
  "idx": 1,
  "istable": 1,
  "links": [],
- "modified": "2021-06-16 19:33:51.099386",
+ "modified": "2021-06-16 19:43:51.099386",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Purchase Invoice Item",
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index e025fc6..b97dc40 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -542,6 +542,7 @@
 		select company, sum(debit_in_account_currency) - sum(credit_in_account_currency)
 		from `tabGL Entry`
 		where party_type = %s and party=%s
+		and is_cancelled = 0
 		group by company""", (party_type, party)))
 
 	for d in companies:
diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py
index 9c9ada8..f1b231b 100644
--- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py
+++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py
@@ -397,6 +397,7 @@
 				{'name': 'Budget', 'chartType': 'bar', 'values': budget_values},
 				{'name': 'Actual Expense', 'chartType': 'bar', 'values': actual_values}
 			]
-		}
+		},
+		'type' : 'bar'
 	}
 
diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
index 7793af7..56a67bb 100644
--- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
+++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
@@ -380,7 +380,7 @@
 		gl_entries = frappe.db.sql("""select gl.posting_date, gl.account, gl.debit, gl.credit, gl.is_opening, gl.company,
 			gl.fiscal_year, gl.debit_in_account_currency, gl.credit_in_account_currency, gl.account_currency,
 			acc.account_name, acc.account_number
-			from `tabGL Entry` gl, `tabAccount` acc where acc.name = gl.account and gl.company = %(company)s
+			from `tabGL Entry` gl, `tabAccount` acc where acc.name = gl.account and gl.company = %(company)s and gl.is_cancelled = 0
 			{additional_conditions} and gl.posting_date <= %(to_date)s and acc.lft >= %(lft)s and acc.rgt <= %(rgt)s
 			order by gl.account, gl.posting_date""".format(additional_conditions=additional_conditions),
 			{
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js
index 84f7868..4a551b8 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.js
+++ b/erpnext/accounts/report/general_ledger/general_ledger.js
@@ -36,16 +36,12 @@
 		{
 			"fieldname":"account",
 			"label": __("Account"),
-			"fieldtype": "Link",
+			"fieldtype": "MultiSelectList",
 			"options": "Account",
-			"get_query": function() {
-				var company = frappe.query_report.get_filter_value('company');
-				return {
-					"doctype": "Account",
-					"filters": {
-						"company": company,
-					}
-				}
+			get_data: function(txt) {
+				return frappe.db.get_link_options('Account', txt, {
+					company: frappe.query_report.get_filter_value("company")
+				});
 			}
 		},
 		{
@@ -135,7 +131,9 @@
 			"label": __("Cost Center"),
 			"fieldtype": "MultiSelectList",
 			get_data: function(txt) {
-				return frappe.db.get_link_options('Cost Center', txt);
+				return frappe.db.get_link_options('Cost Center', txt, {
+					company: frappe.query_report.get_filter_value("company")
+				});
 			}
 		},
 		{
@@ -143,7 +141,9 @@
 			"label": __("Project"),
 			"fieldtype": "MultiSelectList",
 			get_data: function(txt) {
-				return frappe.db.get_link_options('Project', txt);
+				return frappe.db.get_link_options('Project', txt, {
+					company: frappe.query_report.get_filter_value("company")
+				});
 			}
 		},
 		{
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index 562df4f..744ada9 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -49,8 +49,12 @@
 	if not filters.get("from_date") and not filters.get("to_date"):
 		frappe.throw(_("{0} and {1} are mandatory").format(frappe.bold(_("From Date")), frappe.bold(_("To Date"))))
 
-	if filters.get("account") and not account_details.get(filters.account):
-		frappe.throw(_("Account {0} does not exists").format(filters.account))
+	for account in filters.account:
+		if not account_details.get(account):
+			frappe.throw(_("Account {0} does not exists").format(account))
+			
+	if filters.get('account'):
+		filters.account = frappe.parse_json(filters.get('account'))
 
 	if (filters.get("account") and filters.get("group_by") == _('Group by Account')
 		and account_details[filters.account].is_group == 0):
@@ -87,7 +91,19 @@
 		account_currency = None
 
 		if filters.get("account"):
-			account_currency = get_account_currency(filters.account)
+			if len(filters.get("account")) == 1:	
+				account_currency = get_account_currency(filters.account[0])
+			else:
+				currency = get_account_currency(filters.account[0])
+				is_same_account_currency = True
+				for account in filters.get("account"):
+					if get_account_currency(account) != currency:
+						is_same_account_currency = False
+						break
+
+				if is_same_account_currency:
+					account_currency = currency
+
 		elif filters.get("party"):
 			gle_currency = frappe.db.get_value(
 				"GL Entry", {
@@ -205,10 +221,10 @@
 
 def get_conditions(filters):
 	conditions = []
-	if filters.get("account") and not filters.get("include_dimensions"):
-		lft, rgt = frappe.db.get_value("Account", filters["account"], ["lft", "rgt"])
-		conditions.append("""account in (select name from tabAccount
-			where lft>=%s and rgt<=%s and docstatus<2)""" % (lft, rgt))
+
+	if filters.get("account"):
+		filters.account = get_accounts_with_children(filters.account)
+		conditions.append("account in %(account)s")
 
 	if filters.get("cost_center"):
 		filters.cost_center = get_cost_centers_with_children(filters.cost_center)
@@ -266,6 +282,20 @@
 
 	return "and {}".format(" and ".join(conditions)) if conditions else ""
 
+def get_accounts_with_children(accounts):
+	if not isinstance(accounts, list):
+		accounts = [d.strip() for d in accounts.strip().split(',') if d]
+
+	all_accounts = []
+	for d in accounts:
+		if frappe.db.exists("Account", d):
+			lft, rgt = frappe.db.get_value("Account", d, ["lft", "rgt"])
+			children = frappe.get_all("Account", filters={"lft": [">=", lft], "rgt": ["<=", rgt]})
+			all_accounts += [c.name for c in children]
+		else:
+			frappe.throw(_("Account: {0} does not exist").format(d))
+
+	return list(set(all_accounts))
 
 def get_data_with_opening_closing(filters, account_details, accounting_dimensions, gl_entries):
 	data = []
diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js
index 6f1bb28..922cc4a 100644
--- a/erpnext/assets/doctype/asset/asset.js
+++ b/erpnext/assets/doctype/asset/asset.js
@@ -82,24 +82,46 @@
 			if (in_list(["Submitted", "Partially Depreciated", "Fully Depreciated"], frm.doc.status)) {
 				frm.add_custom_button("Transfer Asset", function() {
 					erpnext.asset.transfer_asset(frm);
-				});
+				}, __("Manage"));
 
 				frm.add_custom_button("Scrap Asset", function() {
 					erpnext.asset.scrap_asset(frm);
-				});
+				}, __("Manage"));
 
 				frm.add_custom_button("Sell Asset", function() {
 					frm.trigger("make_sales_invoice");
-				});
+				}, __("Manage"));
 
 			} else if (frm.doc.status=='Scrapped') {
 				frm.add_custom_button("Restore Asset", function() {
 					erpnext.asset.restore_asset(frm);
-				});
+				}, __("Manage"));
+			}
+
+			if (frm.doc.maintenance_required && !frm.doc.maintenance_schedule) {
+				frm.add_custom_button(__("Maintain Asset"), function() {
+					frm.trigger("create_asset_maintenance");
+				}, __("Manage"));
+			}
+		
+			frm.add_custom_button(__("Repair Asset"), function() {
+				frm.trigger("create_asset_repair");
+			}, __("Manage"));
+			
+			if (frm.doc.status != 'Fully Depreciated') {
+				frm.add_custom_button(__("Adjust Asset Value"), function() {
+					frm.trigger("create_asset_adjustment");
+				}, __("Manage"));
+			}
+
+			if (!frm.doc.calculate_depreciation) {
+				frm.add_custom_button(__("Create Depreciation Entry"), function() {
+					frm.trigger("make_journal_entry");
+				}, __("Manage"));
 			}
 
 			if (frm.doc.purchase_receipt || !frm.doc.is_existing_asset) {
-				frm.add_custom_button("General Ledger", function() {
+				frm.add_custom_button("View General Ledger", function() {
 					frappe.route_options = {
 						"voucher_no": frm.doc.name,
 						"from_date": frm.doc.available_for_use_date,
@@ -107,27 +129,9 @@
 						"company": frm.doc.company
 					};
 					frappe.set_route("query-report", "General Ledger");
-				});
+				}, __("Manage"));
 			}
 
-			if (frm.doc.maintenance_required && !frm.doc.maintenance_schedule) {
-				frm.add_custom_button(__("Asset Maintenance"), function() {
-					frm.trigger("create_asset_maintenance");
-				}, __('Create'));
-			}
-			if (frm.doc.status != 'Fully Depreciated') {
-				frm.add_custom_button(__("Asset Value Adjustment"), function() {
-					frm.trigger("create_asset_adjustment");
-				}, __('Create'));
-			}
-
-			if (!frm.doc.calculate_depreciation) {
-				frm.add_custom_button(__("Depreciation Entry"), function() {
-					frm.trigger("make_journal_entry");
-				}, __('Create'));
-			}
-
-			frm.page.set_inner_btn_group_as_primary(__('Create'));
 			frm.trigger("setup_chart");
 		}
 
@@ -304,6 +308,20 @@
 		})
 	},
 
+	create_asset_repair: function(frm) {
+		frappe.call({
+			args: {
+				"asset": frm.doc.name,
+				"asset_name": frm.doc.asset_name
+			},
+			method: "erpnext.assets.doctype.asset.asset.create_asset_repair",
+			callback: function(r) {
+				var doclist = frappe.model.sync(r.message);
+				frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
+			}
+		});
+	},
+
 	create_asset_adjustment: function(frm) {
 		frappe.call({
 			args: {
diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json
index 421b9a6..de06075 100644
--- a/erpnext/assets/doctype/asset/asset.json
+++ b/erpnext/assets/doctype/asset/asset.json
@@ -502,7 +502,7 @@
    "link_fieldname": "asset"
   }
  ],
- "modified": "2021-01-22 12:38:59.091510",
+ "modified": "2021-06-24 14:58:51.097908",
  "modified_by": "Administrator",
  "module": "Assets",
  "name": "Asset",
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index 8799275..66f0bdc 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -168,17 +168,24 @@
 				d.precision("rate_of_depreciation"))
 
 	def make_depreciation_schedule(self):
-		if 'Manual' not in [d.depreciation_method for d in self.finance_books]:
+		if 'Manual' not in [d.depreciation_method for d in self.finance_books] and not self.schedules:
 			self.schedules = []
 
-		if self.get("schedules") or not self.available_for_use_date:
+		if not self.available_for_use_date:
 			return
 
 		for d in self.get('finance_books'):
 			self.validate_asset_finance_books(d)
+			
+			start = self.clear_depreciation_schedule()
 
-			value_after_depreciation = (flt(self.gross_purchase_amount) -
-				flt(self.opening_accumulated_depreciation))
+			# value_after_depreciation - current Asset value
+			if d.value_after_depreciation:
+				value_after_depreciation = (flt(d.value_after_depreciation) -
+					flt(self.opening_accumulated_depreciation)) 
+			else:
+				value_after_depreciation = (flt(self.gross_purchase_amount) -
+					flt(self.opening_accumulated_depreciation)) 
 
 			d.value_after_depreciation = value_after_depreciation
 
@@ -191,7 +198,7 @@
 				number_of_pending_depreciations += 1
 
 			skip_row = False
-			for n in range(number_of_pending_depreciations):
+			for n in range(start, number_of_pending_depreciations):
 				# If depreciation is already completed (for double declining balance)
 				if skip_row: continue
 
@@ -216,11 +223,13 @@
 
 				# For last row
 				elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
-					to_date = add_months(self.available_for_use_date,
-						n * cint(d.frequency_of_depreciation))
+					if not self.flags.increase_in_asset_life:
+						# In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission
+						self.to_date = add_months(self.available_for_use_date,
+							n * cint(d.frequency_of_depreciation))
 
 					depreciation_amount, days, months = self.get_pro_rata_amt(d,
-						depreciation_amount, schedule_date, to_date)
+						depreciation_amount, schedule_date, self.to_date)
 
 					monthly_schedule_date = add_months(schedule_date, 1)
 
@@ -284,10 +293,23 @@
 							"finance_book_id": d.idx
 						})
 
+	# used when depreciation schedule needs to be modified due to increase in asset life
+	def clear_depreciation_schedule(self):
+		start = 0
+		for n in range(len(self.schedules)):
+			if not self.schedules[n].journal_entry:
+				del self.schedules[n:]
+				start = n
+				break
+		return start
+
+
+	# if it returns True, depreciation_amount will not be equal for the first and last rows
 	def check_is_pro_rata(self, row):
 		has_pro_rata = False
-
 		days = date_diff(row.depreciation_start_date, self.available_for_use_date) + 1
+
+		# if frequency_of_depreciation is 12 months, total_days = 365
 		total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation)
 
 		if days < total_days:
@@ -346,11 +368,12 @@
 			if d.finance_book_id not in finance_books:
 				accumulated_depreciation = flt(self.opening_accumulated_depreciation)
 				value_after_depreciation = flt(self.get_value_after_depreciation(d.finance_book_id))
-				finance_books.append(d.finance_book_id)
+				finance_books.append(int(d.finance_book_id))
 
 			depreciation_amount = flt(d.depreciation_amount, d.precision("depreciation_amount"))
 			value_after_depreciation -= flt(depreciation_amount)
 
+			# for the last row, if depreciation method = Straight Line
 			if straight_line_idx and i == max(straight_line_idx) - 1:
 				book = self.get('finance_books')[cint(d.finance_book_id) - 1]
 				depreciation_amount += flt(value_after_depreciation -
@@ -626,8 +649,17 @@
 	return asset_maintenance
 
 @frappe.whitelist()
+def create_asset_repair(asset, asset_name):
+	asset_repair = frappe.new_doc("Asset Repair")
+	asset_repair.update({
+		"asset": asset,
+		"asset_name": asset_name
+	})
+	return asset_repair
+
+@frappe.whitelist()
 def create_asset_adjustment(asset, asset_category, company):
-	asset_maintenance = frappe.new_doc("Asset Value Adjustment")
+	asset_maintenance = frappe.get_doc("Asset Value Adjustment")
 	asset_maintenance.update({
 		"asset": asset,
 		"company": company,
@@ -757,8 +789,15 @@
 	depreciation_left = flt(row.total_number_of_depreciations) - flt(asset.number_of_depreciations_booked)
 
 	if row.depreciation_method in ("Straight Line", "Manual"):
-		depreciation_amount = (flt(row.value_after_depreciation) -
-			flt(row.expected_value_after_useful_life)) / depreciation_left
+		# if the Depreciation Schedule is being prepared for the first time
+		if not asset.flags.increase_in_asset_life:
+			depreciation_amount = (flt(row.value_after_depreciation) -
+				flt(row.expected_value_after_useful_life)) / depreciation_left
+
+		# if the Depreciation Schedule is being modified after Asset Repair
+		else:
+			depreciation_amount = (flt(row.value_after_depreciation) -
+				flt(row.expected_value_after_useful_life)) / (date_diff(asset.to_date, asset.available_for_use_date) / 365)
 	else:
 		depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100))
 
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index 8845f24..59fbe3b 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -125,7 +125,6 @@
 			"frequency_of_depreciation": 12,
 			"depreciation_start_date": "2030-12-31"
 		})
-		asset.insert()
 		self.assertEqual(asset.status, "Draft")
 		asset.save()
 		expected_schedules = [
@@ -154,9 +153,8 @@
 			"frequency_of_depreciation": 12,
 			"depreciation_start_date": '2030-12-31'
 		})
-		asset.insert()
-		self.assertEqual(asset.status, "Draft")
 		asset.save()
+		self.assertEqual(asset.status, "Draft")
 
 		expected_schedules = [
 			['2030-12-31', 66667.00, 66667.00],
@@ -185,7 +183,7 @@
 			"frequency_of_depreciation": 12,
 			"depreciation_start_date": "2030-12-31"
 		})
-		asset.insert()
+		asset.save()
 		self.assertEqual(asset.status, "Draft")
 
 		expected_schedules = [
@@ -216,7 +214,6 @@
 			"depreciation_start_date": "2030-12-31"
 		})
 
-		asset.insert()
 		asset.save()
 
 		expected_schedules = [
@@ -247,7 +244,6 @@
 			"frequency_of_depreciation": 10,
 			"depreciation_start_date": "2020-12-31"
 		})
-		asset.insert()
 		asset.submit()
 		asset.load_from_db()
 		self.assertEqual(asset.status, "Submitted")
@@ -350,7 +346,6 @@
 			"frequency_of_depreciation": 10,
 			"depreciation_start_date": "2020-12-31"
 		})
-		asset.insert()
 		asset.submit()
 		post_depreciation_entries(date="2021-01-01")
 
@@ -380,7 +375,6 @@
 			"total_number_of_depreciations": 10,
 			"frequency_of_depreciation": 1
 		})
-		asset.insert()
 		asset.submit()
 
 		post_depreciation_entries(date=add_months('2020-01-01', 4))
@@ -424,7 +418,6 @@
 			"frequency_of_depreciation": 10,
 			"depreciation_start_date": "2020-12-31"
 		})
-		asset.insert()
 		asset.submit()
 		post_depreciation_entries(date="2021-01-01")
 
@@ -468,7 +461,7 @@
 			"total_number_of_depreciations": 3,
 			"frequency_of_depreciation": 10
 		})
-		asset.insert()
+		asset.save()
 		accumulated_depreciation_after_full_schedule = \
 			max(d.accumulated_depreciation_amount for d in asset.get("schedules"))
 
@@ -699,7 +692,7 @@
 		"item_code": args.item_code or "Macbook Pro",
 		"company": args.company or"_Test Company",
 		"purchase_date": "2015-01-01",
-		"calculate_depreciation": 0,
+		"calculate_depreciation": args.calculate_depreciation or 0,
 		"gross_purchase_amount": 100000,
 		"purchase_receipt_amount": 100000,
 		"expected_value_after_useful_life": 10000,
@@ -707,9 +700,16 @@
 		"available_for_use_date": "2020-06-06",
 		"location": "Test Location",
 		"asset_owner": "Company",
-		"is_existing_asset": args.is_existing_asset or 0
+		"is_existing_asset": 1
 	})
 
+	if asset.calculate_depreciation:
+		asset.append("finance_books", {
+			"depreciation_method": "Straight Line",
+			"frequency_of_depreciation": 12,
+			"total_number_of_depreciations": 5
+		})
+
 	try:
 		asset.save()
 	except frappe.DuplicateEntryError:
diff --git a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json
index d9b7b69..e5a5f19 100644
--- a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json
+++ b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json
@@ -67,7 +67,6 @@
   {
    "fieldname": "value_after_depreciation",
    "fieldtype": "Currency",
-   "hidden": 1,
    "label": "Value After Depreciation",
    "no_copy": 1,
    "options": "Company:company:default_currency",
@@ -85,7 +84,7 @@
  "index_web_pages_for_search": 1,
  "istable": 1,
  "links": [],
- "modified": "2020-11-05 16:30:09.213479",
+ "modified": "2021-06-17 12:59:05.743683",
  "modified_by": "Administrator",
  "module": "Assets",
  "name": "Asset Finance Book",
diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js
index 4ba2b44..1cebfff 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.js
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.js
@@ -2,6 +2,45 @@
 // For license information, please see license.txt
 
 frappe.ui.form.on('Asset Repair', {
+	setup: function(frm) {
+		frm.fields_dict.cost_center.get_query = function(doc) {
+			return {
+				filters: {
+					'is_group': 0,
+					'company': doc.company
+				}
+			};
+		};
+
+		frm.fields_dict.project.get_query = function(doc) {
+			return {
+				filters: {
+					'company': doc.company
+				}
+			};
+		};
+
+		frm.fields_dict.warehouse.get_query = function(doc) {
+			return {
+				filters: {
+					'is_group': 0,
+					'company': doc.company
+				}
+			};
+		};
+	},
+
+	refresh: function(frm) {
+		if (frm.doc.docstatus) {
+			frm.add_custom_button("View General Ledger", function() {
+				frappe.route_options = {
+					"voucher_no": frm.doc.name
+				};
+				frappe.set_route("query-report", "General Ledger");
+			});
+		}
+	},
+
 	repair_status: (frm) => {
 		if (frm.doc.completion_date && frm.doc.repair_status == "Completed") {
 			frappe.call ({
@@ -17,5 +56,16 @@
 				}
 			});
 		}
+
+		if (frm.doc.repair_status == "Completed") {
+			frm.set_value('completion_date', frappe.datetime.now_datetime());
+		}				
 	}
 });
+
+frappe.ui.form.on('Asset Repair Consumed Item', {
+	consumed_quantity: function(frm, cdt, cdn) {
+		var row = locals[cdt][cdn];
+		frappe.model.set_value(cdt, cdn, 'total_value', row.consumed_quantity * row.valuation_rate);
+	},
+});
\ No newline at end of file
diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json
index d338fc0..ba31898 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.json
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.json
@@ -7,39 +7,44 @@
  "editable_grid": 1,
  "engine": "InnoDB",
  "field_order": [
-  "naming_series",
-  "asset_name",
+  "asset",
+  "company",
   "column_break_2",
-  "item_code",
-  "item_name",
+  "asset_name",
+  "naming_series",
   "section_break_5",
   "failure_date",
-  "assign_to",
-  "assign_to_name",
+  "repair_status",
   "column_break_6",
   "completion_date",
-  "repair_status",
+  "accounting_dimensions_section",
+  "cost_center",
+  "column_break_14",
+  "project",
+  "accounting_details",
   "repair_cost",
+  "capitalize_repair_cost",
+  "stock_consumption",
+  "column_break_8",
+  "purchase_invoice",
+  "stock_consumption_details_section",
+  "warehouse",
+  "stock_items",
+  "total_repair_cost",
+  "stock_entry",
+  "asset_depreciation_details_section",
+  "increase_in_asset_life",
   "section_break_9",
   "description",
   "column_break_9",
   "actions_performed",
-  "section_break_17",
+  "section_break_23",
   "downtime",
   "column_break_19",
   "amended_from"
  ],
  "fields": [
   {
-   "columns": 1,
-   "fieldname": "asset_name",
-   "fieldtype": "Link",
-   "in_list_view": 1,
-   "label": "Asset",
-   "options": "Asset",
-   "reqd": 1
-  },
-  {
    "fieldname": "naming_series",
    "fieldtype": "Select",
    "label": "Series",
@@ -51,18 +56,6 @@
    "fieldtype": "Column Break"
   },
   {
-   "fetch_from": "asset_name.item_code",
-   "fieldname": "item_code",
-   "fieldtype": "Read Only",
-   "label": "Item Code"
-  },
-  {
-   "fetch_from": "asset_name.item_name",
-   "fieldname": "item_name",
-   "fieldtype": "Read Only",
-   "label": "Item Name"
-  },
-  {
    "fieldname": "section_break_5",
    "fieldtype": "Section Break",
    "label": "Repair Details"
@@ -75,32 +68,19 @@
    "reqd": 1
   },
   {
-   "allow_on_submit": 1,
-   "fieldname": "assign_to",
-   "fieldtype": "Link",
-   "label": "Assign To",
-   "options": "User"
-  },
-  {
-   "allow_on_submit": 1,
-   "fetch_from": "assign_to.full_name",
-   "fieldname": "assign_to_name",
-   "fieldtype": "Read Only",
-   "label": "Assign To Name"
-  },
-  {
    "fieldname": "column_break_6",
    "fieldtype": "Column Break"
   },
   {
-   "allow_on_submit": 1,
+   "depends_on": "eval:!doc.__islocal",
    "fieldname": "completion_date",
    "fieldtype": "Datetime",
-   "label": "Completion Date"
+   "label": "Completion Date",
+   "no_copy": 1
   },
   {
-   "allow_on_submit": 1,
    "default": "Pending",
+   "depends_on": "eval:!doc.__islocal",
    "fieldname": "repair_status",
    "fieldtype": "Select",
    "label": "Repair Status",
@@ -116,25 +96,18 @@
   {
    "fieldname": "description",
    "fieldtype": "Long Text",
-   "label": "Error Description",
-   "reqd": 1
+   "label": "Error Description"
   },
   {
    "fieldname": "column_break_9",
    "fieldtype": "Column Break"
   },
   {
-   "allow_on_submit": 1,
    "fieldname": "actions_performed",
    "fieldtype": "Long Text",
    "label": "Actions performed"
   },
   {
-   "fieldname": "section_break_17",
-   "fieldtype": "Section Break"
-  },
-  {
-   "allow_on_submit": 1,
    "fieldname": "downtime",
    "fieldtype": "Data",
    "in_list_view": 1,
@@ -146,7 +119,7 @@
    "fieldtype": "Column Break"
   },
   {
-   "allow_on_submit": 1,
+   "default": "0",
    "fieldname": "repair_cost",
    "fieldtype": "Currency",
    "label": "Repair Cost"
@@ -159,12 +132,139 @@
    "options": "Asset Repair",
    "print_hide": 1,
    "read_only": 1
+  },
+  {
+   "columns": 1,
+   "fieldname": "asset",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "label": "Asset",
+   "options": "Asset",
+   "reqd": 1
+  },
+  {
+   "fetch_from": "asset.asset_name",
+   "fieldname": "asset_name",
+   "fieldtype": "Read Only",
+   "label": "Asset Name"
+  },
+  {
+   "fieldname": "column_break_8",
+   "fieldtype": "Column Break"
+  },
+  {
+   "default": "0",
+   "depends_on": "eval:!doc.__islocal",
+   "fieldname": "capitalize_repair_cost",
+   "fieldtype": "Check",
+   "label": "Capitalize Repair Cost"
+  },
+  {
+   "fieldname": "accounting_details",
+   "fieldtype": "Section Break",
+   "label": "Accounting Details"
+  },
+  {
+   "fieldname": "stock_items",
+   "fieldtype": "Table",
+   "label": "Stock Items",
+   "mandatory_depends_on": "stock_consumption",
+   "options": "Asset Repair Consumed Item"
+  },
+  {
+   "fieldname": "section_break_23",
+   "fieldtype": "Section Break"
+  },
+  {
+   "collapsible": 1,
+   "fieldname": "accounting_dimensions_section",
+   "fieldtype": "Section Break",
+   "label": "Accounting Dimensions"
+  },
+  {
+   "fieldname": "cost_center",
+   "fieldtype": "Link",
+   "label": "Cost Center",
+   "options": "Cost Center"
+  },
+  {
+   "fieldname": "project",
+   "fieldtype": "Link",
+   "label": "Project",
+   "options": "Project"
+  },
+  {
+   "fieldname": "column_break_14",
+   "fieldtype": "Column Break"
+  },
+  {
+   "default": "0",
+   "depends_on": "eval:!doc.__islocal",
+   "fieldname": "stock_consumption",
+   "fieldtype": "Check",
+   "label": "Stock Consumed During Repair"
+  },
+  {
+   "depends_on": "stock_consumption",
+   "fieldname": "stock_consumption_details_section",
+   "fieldtype": "Section Break",
+   "label": "Stock Consumption Details"
+  },
+  {
+   "depends_on": "eval: doc.stock_consumption && doc.total_repair_cost > 0",
+   "description": "Sum of Repair Cost and Value of Consumed Stock Items.",
+   "fieldname": "total_repair_cost",
+   "fieldtype": "Currency",
+   "label": "Total Repair Cost",
+   "read_only": 1
+  },
+  {
+   "depends_on": "stock_consumption",
+   "fieldname": "warehouse",
+   "fieldtype": "Link",
+   "label": "Warehouse",
+   "options": "Warehouse"
+  },
+  {
+   "depends_on": "capitalize_repair_cost",
+   "fieldname": "asset_depreciation_details_section",
+   "fieldtype": "Section Break",
+   "label": "Asset Depreciation Details"
+  },
+  {
+   "fieldname": "increase_in_asset_life",
+   "fieldtype": "Int",
+   "label": "Increase In Asset Life(Months)",
+   "no_copy": 1
+  },
+  {
+   "depends_on": "eval:!doc.__islocal",
+   "fieldname": "purchase_invoice",
+   "fieldtype": "Link",
+   "label": "Purchase Invoice",
+   "mandatory_depends_on": "eval: doc.repair_status == 'Completed' && doc.repair_cost > 0",
+   "no_copy": 1,
+   "options": "Purchase Invoice"
+  },
+  {
+   "fetch_from": "asset.company",
+   "fieldname": "company",
+   "fieldtype": "Link",
+   "label": "Company",
+   "options": "Company"
+  },
+  {
+   "fieldname": "stock_entry",
+   "fieldtype": "Link",
+   "label": "Stock Entry",
+   "options": "Stock Entry",
+   "read_only": 1
   }
  ],
  "index_web_pages_for_search": 1,
  "is_submittable": 1,
  "links": [],
- "modified": "2021-01-22 15:08:12.495850",
+ "modified": "2021-06-25 13:14:38.307723",
  "modified_by": "Administrator",
  "module": "Assets",
  "name": "Asset Repair",
@@ -203,6 +303,7 @@
  ],
  "sort_field": "modified",
  "sort_order": "DESC",
+ "title_field": "asset_name",
  "track_changes": 1,
  "track_seen": 1
 }
\ No newline at end of file
diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py
index 049b931..d32fdf7 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.py
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.py
@@ -5,16 +5,252 @@
 from __future__ import unicode_literals
 import frappe
 from frappe import _
-from frappe.utils import time_diff_in_hours
-from frappe.model.document import Document
+from frappe.utils import time_diff_in_hours, getdate, add_months, flt, cint
+from erpnext.accounts.general_ledger import make_gl_entries
+from erpnext.assets.doctype.asset.asset import get_asset_account
+from erpnext.controllers.accounts_controller import AccountsController
 
-class AssetRepair(Document):
+class AssetRepair(AccountsController):
 	def validate(self):
-		if self.repair_status == "Completed" and not self.completion_date:
-			frappe.throw(_("Please select Completion Date for Completed Repair"))
+		self.asset_doc = frappe.get_doc('Asset', self.asset)
+		self.update_status()
 
+		if self.get('stock_items'):
+			self.set_total_value()
+		self.calculate_total_repair_cost()
+		
+	def update_status(self):
+		if self.repair_status == 'Pending':
+			frappe.db.set_value('Asset', self.asset, 'status', 'Out of Order')
+		else:
+			self.asset_doc.set_status()
+
+	def set_total_value(self):
+		for item in self.get('stock_items'):
+			item.total_value = flt(item.valuation_rate) * flt(item.consumed_quantity)
+
+	def calculate_total_repair_cost(self):
+		self.total_repair_cost = flt(self.repair_cost)
+
+		total_value_of_stock_consumed = self.get_total_value_of_stock_consumed()
+		self.total_repair_cost += total_value_of_stock_consumed
+
+	def before_submit(self):
+		self.check_repair_status()
+
+		if self.get('stock_consumption') or self.get('capitalize_repair_cost'):
+			self.increase_asset_value()
+		if self.get('stock_consumption'):
+			self.check_for_stock_items_and_warehouse()
+			self.decrease_stock_quantity()
+		if self.get('capitalize_repair_cost'):
+			self.make_gl_entries()
+			if frappe.db.get_value('Asset', self.asset, 'calculate_depreciation') and self.increase_in_asset_life:
+				self.modify_depreciation_schedule()
+
+		self.asset_doc.flags.ignore_validate_update_after_submit = True
+		self.asset_doc.prepare_depreciation_data()
+		self.asset_doc.save()
+
+	def before_cancel(self):
+		self.asset_doc = frappe.get_doc('Asset', self.asset)
+
+		if self.get('stock_consumption') or self.get('capitalize_repair_cost'):
+			self.decrease_asset_value()
+		if self.get('stock_consumption'):
+			self.increase_stock_quantity()
+		if self.get('capitalize_repair_cost'):
+			self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
+			self.make_gl_entries(cancel=True)
+			if frappe.db.get_value('Asset', self.asset, 'calculate_depreciation') and self.increase_in_asset_life:
+				self.revert_depreciation_schedule_on_cancellation()
+
+		self.asset_doc.flags.ignore_validate_update_after_submit = True
+		self.asset_doc.prepare_depreciation_data()
+		self.asset_doc.save()
+
+	def check_repair_status(self):
+		if self.repair_status == "Pending":
+			frappe.throw(_("Please update Repair Status."))
+
+	def check_for_stock_items_and_warehouse(self):
+		if not self.get('stock_items'):
+			frappe.throw(_("Please enter Stock Items consumed during the Repair."), title=_("Missing Items"))
+		if not self.warehouse:
+			frappe.throw(_("Please enter Warehouse from which Stock Items consumed during the Repair were taken."), title=_("Missing Warehouse"))
+
+	def increase_asset_value(self):
+		total_value_of_stock_consumed = self.get_total_value_of_stock_consumed()
+
+		if self.asset_doc.calculate_depreciation:
+			for row in self.asset_doc.finance_books:
+				row.value_after_depreciation += total_value_of_stock_consumed
+
+				if self.capitalize_repair_cost:
+					row.value_after_depreciation += self.repair_cost
+
+	def decrease_asset_value(self):
+		total_value_of_stock_consumed = self.get_total_value_of_stock_consumed()
+
+		if self.asset_doc.calculate_depreciation:
+			for row in self.asset_doc.finance_books:
+				row.value_after_depreciation -= total_value_of_stock_consumed
+
+				if self.capitalize_repair_cost:
+					row.value_after_depreciation -= self.repair_cost
+		
+	def get_total_value_of_stock_consumed(self):
+		total_value_of_stock_consumed = 0
+		if self.get('stock_consumption'):
+			for item in self.get('stock_items'):
+				total_value_of_stock_consumed += item.total_value
+
+		return total_value_of_stock_consumed
+
+	def decrease_stock_quantity(self):
+		stock_entry = frappe.get_doc({
+			"doctype": "Stock Entry",
+			"stock_entry_type": "Material Issue",
+			"company": self.company
+		})
+
+		for stock_item in self.get('stock_items'):
+			stock_entry.append('items', {
+				"s_warehouse": self.warehouse,
+				"item_code": stock_item.item,
+				"qty": stock_item.consumed_quantity,
+				"basic_rate": stock_item.valuation_rate
+			})
+
+		stock_entry.insert()
+		stock_entry.submit()
+
+		self.db_set('stock_entry', stock_entry.name)
+
+	def increase_stock_quantity(self):
+		stock_entry = frappe.get_doc('Stock Entry', self.stock_entry)
+		stock_entry.flags.ignore_links = True
+		stock_entry.cancel()
+
+	def make_gl_entries(self, cancel=False):
+		if flt(self.repair_cost) > 0:
+			gl_entries = self.get_gl_entries()
+			make_gl_entries(gl_entries, cancel)
+
+	def get_gl_entries(self):
+		gl_entries = []
+		repair_and_maintenance_account = frappe.db.get_value('Company', self.company, 'repair_and_maintenance_account')
+		fixed_asset_account = get_asset_account("fixed_asset_account", asset=self.asset, company=self.company)
+		expense_account = frappe.get_doc('Purchase Invoice', self.purchase_invoice).items[0].expense_account	
+
+		gl_entries.append(
+			self.get_gl_dict({
+				"account": expense_account,
+				"credit": self.repair_cost,
+				"credit_in_account_currency": self.repair_cost,
+				"against": repair_and_maintenance_account,
+				"voucher_type": self.doctype,		
+				"voucher_no": self.name,
+				"cost_center": self.cost_center,
+				"posting_date": getdate(),
+				"company": self.company
+			}, item=self)
+		)
+
+		if self.get('stock_consumption'):
+			# creating GL Entries for each row in Stock Items based on the Stock Entry created for it
+			stock_entry = frappe.get_doc('Stock Entry', self.stock_entry)
+			for item in stock_entry.items:
+				gl_entries.append(
+					self.get_gl_dict({
+						"account": item.expense_account,
+						"credit": item.amount,
+						"credit_in_account_currency": item.amount,
+						"against": repair_and_maintenance_account,
+						"voucher_type": self.doctype,		
+						"voucher_no": self.name,
+						"cost_center": self.cost_center,
+						"posting_date": getdate(),
+						"company": self.company
+					}, item=self)
+				)
+
+		gl_entries.append(
+			self.get_gl_dict({
+				"account": fixed_asset_account,
+				"debit": self.total_repair_cost,
+				"debit_in_account_currency": self.total_repair_cost,
+				"against": expense_account,
+				"voucher_type": self.doctype,
+				"voucher_no": self.name,
+				"cost_center": self.cost_center,
+				"posting_date": getdate(),
+				"against_voucher_type": "Purchase Invoice",
+				"against_voucher": self.purchase_invoice,
+				"company": self.company
+			}, item=self)
+		)
+
+		return gl_entries
+
+	def modify_depreciation_schedule(self):
+		for row in self.asset_doc.finance_books:
+			row.total_number_of_depreciations += self.increase_in_asset_life/row.frequency_of_depreciation
+
+			self.asset_doc.flags.increase_in_asset_life = False
+			extra_months = self.increase_in_asset_life % row.frequency_of_depreciation
+			if extra_months != 0:
+				self.calculate_last_schedule_date(self.asset_doc, row, extra_months)
+
+	# to help modify depreciation schedule when increase_in_asset_life is not a multiple of frequency_of_depreciation
+	def calculate_last_schedule_date(self, asset, row, extra_months):
+		asset.flags.increase_in_asset_life = True
+		number_of_pending_depreciations = cint(row.total_number_of_depreciations) - \
+			cint(asset.number_of_depreciations_booked)
+
+		# the Schedule Date in the final row of the old Depreciation Schedule
+		last_schedule_date = asset.schedules[len(asset.schedules)-1].schedule_date
+
+		# the Schedule Date in the final row of the new Depreciation Schedule
+		asset.to_date = add_months(last_schedule_date, extra_months)
+
+		# the latest possible date at which the depreciation can occur, without increasing the Total Number of Depreciations
+		# if depreciations happen yearly and the Depreciation Posting Date is 01-01-2020, this could be 01-01-2021, 01-01-2022...
+		schedule_date = add_months(row.depreciation_start_date,
+			number_of_pending_depreciations * cint(row.frequency_of_depreciation))
+
+		if asset.to_date > schedule_date:
+			row.total_number_of_depreciations += 1
+
+	def revert_depreciation_schedule_on_cancellation(self):
+		for row in self.asset_doc.finance_books:
+			row.total_number_of_depreciations -= self.increase_in_asset_life/row.frequency_of_depreciation
+
+			self.asset_doc.flags.increase_in_asset_life = False
+			extra_months = self.increase_in_asset_life % row.frequency_of_depreciation
+			if extra_months != 0:
+				self.calculate_last_schedule_date_before_modification(self.asset_doc, row, extra_months)
+
+	def calculate_last_schedule_date_before_modification(self, asset, row, extra_months):
+		asset.flags.increase_in_asset_life = True
+		number_of_pending_depreciations = cint(row.total_number_of_depreciations) - \
+			cint(asset.number_of_depreciations_booked)
+
+		# the Schedule Date in the final row of the modified Depreciation Schedule
+		last_schedule_date = asset.schedules[len(asset.schedules)-1].schedule_date
+
+		# the Schedule Date in the final row of the original Depreciation Schedule
+		asset.to_date = add_months(last_schedule_date, -extra_months)
+
+		# the latest possible date at which the depreciation can occur, without decreasing the Total Number of Depreciations
+		# if depreciations happen yearly and the Depreciation Posting Date is 01-01-2020, this could be 01-01-2021, 01-01-2022...
+		schedule_date = add_months(row.depreciation_start_date,
+			(number_of_pending_depreciations - 1) * cint(row.frequency_of_depreciation))
+
+		if asset.to_date < schedule_date:
+			row.total_number_of_depreciations -= 1
 
 @frappe.whitelist()
 def get_downtime(failure_date, completion_date):
 	downtime = time_diff_in_hours(completion_date, failure_date)
-	return round(downtime, 2)
\ No newline at end of file
+	return round(downtime, 2)
diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py
index 3d325a9..30bbb37 100644
--- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py
+++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py
@@ -2,8 +2,167 @@
 # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
 # See license.txt
 from __future__ import unicode_literals
-
+import frappe
+from frappe.utils import nowdate, flt
 import unittest
+from erpnext.assets.doctype.asset.test_asset import create_asset_data, create_asset, set_depreciation_settings_in_company
 
 class TestAssetRepair(unittest.TestCase):
-	pass
+	def setUp(self):
+		set_depreciation_settings_in_company()
+		create_asset_data()
+		frappe.db.sql("delete from `tabTax Rule`")
+
+	def test_update_status(self):
+		asset = create_asset()
+		initial_status = asset.status
+		asset_repair = create_asset_repair(asset = asset)
+
+		if asset_repair.repair_status == "Pending":
+			asset.reload()
+			self.assertEqual(asset.status, "Out of Order")
+
+		asset_repair.repair_status = "Completed"
+		asset_repair.save()
+		asset_status = frappe.db.get_value("Asset", asset_repair.asset, "status")
+		self.assertEqual(asset_status, initial_status)
+
+	def test_stock_item_total_value(self):
+		asset_repair = create_asset_repair(stock_consumption = 1)
+
+		for item in asset_repair.stock_items:
+			total_value = flt(item.valuation_rate) * flt(item.consumed_quantity)
+			self.assertEqual(item.total_value, total_value)
+
+	def test_total_repair_cost(self):
+		asset_repair = create_asset_repair(stock_consumption = 1)
+
+		total_repair_cost = asset_repair.repair_cost
+		self.assertEqual(total_repair_cost, asset_repair.repair_cost)
+		for item in asset_repair.stock_items:
+			total_repair_cost += item.total_value
+			
+		self.assertEqual(total_repair_cost, asset_repair.total_repair_cost)
+
+	def test_repair_status_after_submit(self):
+		asset_repair = create_asset_repair(submit = 1)
+		self.assertNotEqual(asset_repair.repair_status, "Pending")
+
+	def test_stock_items(self):
+		asset_repair = create_asset_repair(stock_consumption = 1)
+		self.assertTrue(asset_repair.stock_consumption)
+		self.assertTrue(asset_repair.stock_items)
+
+	def test_warehouse(self):
+		asset_repair = create_asset_repair(stock_consumption = 1)
+		self.assertTrue(asset_repair.stock_consumption)
+		self.assertTrue(asset_repair.warehouse)
+
+	def test_decrease_stock_quantity(self):
+		asset_repair = create_asset_repair(stock_consumption = 1, submit = 1)
+		stock_entry = frappe.get_last_doc('Stock Entry')
+
+		self.assertEqual(stock_entry.stock_entry_type, "Material Issue")
+		self.assertEqual(stock_entry.items[0].s_warehouse, asset_repair.warehouse)
+		self.assertEqual(stock_entry.items[0].item_code, asset_repair.stock_items[0].item)
+		self.assertEqual(stock_entry.items[0].qty, asset_repair.stock_items[0].consumed_quantity)
+
+	def test_increase_in_asset_value_due_to_stock_consumption(self):
+		asset = create_asset(calculate_depreciation = 1)
+		initial_asset_value = get_asset_value(asset)
+		asset_repair = create_asset_repair(asset= asset, stock_consumption = 1, submit = 1)
+		asset.reload()
+
+		increase_in_asset_value = get_asset_value(asset) - initial_asset_value
+		self.assertEqual(asset_repair.stock_items[0].total_value, increase_in_asset_value)
+
+	def test_increase_in_asset_value_due_to_repair_cost_capitalisation(self):
+		asset = create_asset(calculate_depreciation = 1)
+		initial_asset_value = get_asset_value(asset)
+		asset_repair = create_asset_repair(asset= asset, capitalize_repair_cost = 1, submit = 1)
+		asset.reload()
+
+		increase_in_asset_value = get_asset_value(asset) - initial_asset_value
+		self.assertEqual(asset_repair.repair_cost, increase_in_asset_value)
+
+	def test_purchase_invoice(self):
+		asset_repair = create_asset_repair(capitalize_repair_cost = 1, submit = 1)
+		self.assertTrue(asset_repair.purchase_invoice)
+
+	def test_gl_entries(self):
+		asset_repair = create_asset_repair(capitalize_repair_cost = 1, submit = 1)
+		gl_entry = frappe.get_last_doc('GL Entry')
+		self.assertEqual(asset_repair.name, gl_entry.voucher_no)
+
+	def test_increase_in_asset_life(self):
+		asset = create_asset(calculate_depreciation = 1)
+		initial_num_of_depreciations = num_of_depreciations(asset)
+		create_asset_repair(asset= asset, capitalize_repair_cost = 1, submit = 1)
+		asset.reload()
+	
+		self.assertEqual((initial_num_of_depreciations + 1), num_of_depreciations(asset))
+		self.assertEqual(asset.schedules[-1].accumulated_depreciation_amount, asset.finance_books[0].value_after_depreciation)
+
+def get_asset_value(asset):
+	return asset.finance_books[0].value_after_depreciation
+
+def num_of_depreciations(asset):
+	return asset.finance_books[0].total_number_of_depreciations
+
+def create_asset_repair(**args):
+	from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
+	from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
+
+	args = frappe._dict(args)
+
+	if args.asset:
+		asset = args.asset
+	else:
+		asset = create_asset(is_existing_asset = 1)
+	asset_repair = frappe.new_doc("Asset Repair")
+	asset_repair.update({
+		"asset": asset.name,
+		"asset_name": asset.asset_name,
+		"failure_date": nowdate(),
+		"description": "Test Description",
+		"repair_cost": 0,
+		"company": asset.company
+	})
+
+	if args.stock_consumption:
+		asset_repair.stock_consumption = 1
+		asset_repair.warehouse = create_warehouse("Test Warehouse", company = asset.company)
+		asset_repair.append("stock_items", {
+			"item": args.item or args.item_code or "_Test Item",
+			"valuation_rate": args.rate if args.get("rate") is not None else 100,
+			"consumed_quantity": args.qty or 1
+		})
+
+	asset_repair.insert(ignore_if_duplicate=True)
+	
+	if args.submit:
+		asset_repair.repair_status = "Completed"
+		asset_repair.cost_center = "_Test Cost Center - _TC"
+
+		if args.stock_consumption:
+			stock_entry = frappe.get_doc({
+				"doctype": "Stock Entry",
+				"stock_entry_type": "Material Receipt",
+				"company": asset.company
+			})
+			stock_entry.append('items', {
+				"t_warehouse": asset_repair.warehouse,
+				"item_code": asset_repair.stock_items[0].item,
+				"qty": asset_repair.stock_items[0].consumed_quantity
+			})
+			stock_entry.submit()
+
+		if args.capitalize_repair_cost:
+			asset_repair.capitalize_repair_cost = 1
+			asset_repair.repair_cost = 1000
+			if asset.calculate_depreciation:
+				asset_repair.increase_in_asset_life = 12
+			asset_repair.purchase_invoice = make_purchase_invoice().name
+
+		asset_repair.submit()
+	return asset_repair
\ No newline at end of file
diff --git a/erpnext/assets/doctype/asset_repair_consumed_item/__init__.py b/erpnext/assets/doctype/asset_repair_consumed_item/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/assets/doctype/asset_repair_consumed_item/__init__.py
diff --git a/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json
new file mode 100644
index 0000000..528f0ec
--- /dev/null
+++ b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json
@@ -0,0 +1,55 @@
+{
+ "actions": [],
+ "creation": "2021-05-12 02:41:54.161024",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "item",
+  "valuation_rate",
+  "consumed_quantity",
+  "total_value"
+ ],
+ "fields": [
+  {
+   "fieldname": "item",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "label": "Item",
+   "options": "Item"
+  },
+  {
+   "fetch_from": "item.valuation_rate",
+   "fieldname": "valuation_rate",
+   "fieldtype": "Currency",
+   "in_list_view": 1,
+   "label": "Valuation Rate",
+   "read_only": 1
+  },
+  {
+   "fieldname": "consumed_quantity",
+   "fieldtype": "Data",
+   "in_list_view": 1,
+   "label": "Consumed Quantity"
+  },
+  {
+   "fieldname": "total_value",
+   "fieldtype": "Currency",
+   "in_list_view": 1,
+   "label": "Total Value",
+   "read_only": 1
+  }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-05-12 03:19:55.006300",
+ "modified_by": "Administrator",
+ "module": "Assets",
+ "name": "Asset Repair Consumed Item",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.py b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.py
new file mode 100644
index 0000000..fa22a57
--- /dev/null
+++ b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+class AssetRepairConsumedItem(Document):
+	pass
diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json
index 838a9ab..b9c77d5 100644
--- a/erpnext/buying/doctype/buying_settings/buying_settings.json
+++ b/erpnext/buying/doctype/buying_settings/buying_settings.json
@@ -123,7 +123,7 @@
  "index_web_pages_for_search": 1,
  "issingle": 1,
  "links": [],
- "modified": "2021-06-23 19:40:00.120822",
+ "modified": "2021-06-24 10:38:28.934525",
  "modified_by": "Administrator",
  "module": "Buying",
  "name": "Buying Settings",
diff --git a/erpnext/change_log/v13/v13_6_0.md b/erpnext/change_log/v13/v13_6_0.md
new file mode 100644
index 0000000..d881b27
--- /dev/null
+++ b/erpnext/change_log/v13/v13_6_0.md
@@ -0,0 +1,72 @@
+# Version 13.6.0 Release Notes
+
+### Features & Enhancements
+
+- Job Card Enhancements ([#24523](https://github.com/frappe/erpnext/pull/24523))
+- Implement multi-account selection in General Ledger([#26044](https://github.com/frappe/erpnext/pull/26044))
+- Fetching of qty as per received qty from PR to PI ([#26184](https://github.com/frappe/erpnext/pull/26184))
+- Subcontract code refactor and enhancement ([#25878](https://github.com/frappe/erpnext/pull/25878))
+- Employee Grievance ([#25705](https://github.com/frappe/erpnext/pull/25705))
+- Add Inactive status to Employee ([#26030](https://github.com/frappe/erpnext/pull/26030))
+- Incorrect valuation rate report for serialized items ([#25696](https://github.com/frappe/erpnext/pull/25696))
+- Update cost updates operation time and hour rates in BOM ([#25891](https://github.com/frappe/erpnext/pull/25891))
+
+### Fixes
+
+- Precision rate for packed items in internal transfers ([#26046](https://github.com/frappe/erpnext/pull/26046))
+- User is not able to change item tax template ([#26176](https://github.com/frappe/erpnext/pull/26176))
+- Insufficient permission for Dunning error ([#26092](https://github.com/frappe/erpnext/pull/26092))
+- Validate Product Bundle for existing transactions before deletion ([#25978](https://github.com/frappe/erpnext/pull/25978))
+- Auto unlink warehouse from item on delete ([#26073](https://github.com/frappe/erpnext/pull/26073))
+- Employee Inactive status implications ([#26245](https://github.com/frappe/erpnext/pull/26245))
+- Fetch batch items in stock reconciliation ([#26230](https://github.com/frappe/erpnext/pull/26230))
+- Disabled cancellation for sales order if linked to drafted sales invoice ([#26125](https://github.com/frappe/erpnext/pull/26125))
+- Sort website products by weightage mentioned in Item master ([#26134](https://github.com/frappe/erpnext/pull/26134))
+- Added freeze when trying to stop work order (#26192) ([#26196](https://github.com/frappe/erpnext/pull/26196))
+- Accounting Dimensions for payroll entry accrual Journal Entry ([#26083](https://github.com/frappe/erpnext/pull/26083))
+- Staffing plan vacancies data type issue ([#25941](https://github.com/frappe/erpnext/pull/25941))
+- Unable to enter score in Assessment Result details grid ([#25945](https://github.com/frappe/erpnext/pull/25945))
+- Report Subcontracted Raw Materials to be Transferred ([#26011](https://github.com/frappe/erpnext/pull/26011))
+- Label for enabling ledger posting of change amount ([#26070](https://github.com/frappe/erpnext/pull/26070))
+- Training event ([#26071](https://github.com/frappe/erpnext/pull/26071))
+- Rate not able to change in purchase order ([#26122](https://github.com/frappe/erpnext/pull/26122))
+- Error while fetching item taxes ([#26220](https://github.com/frappe/erpnext/pull/26220))
+- Check for duplicate payment terms in Payment Term Template ([#26003](https://github.com/frappe/erpnext/pull/26003))
+- Removed values out of sync validation from stock transactions ([#26229](https://github.com/frappe/erpnext/pull/26229))
+- Fetching employee in payroll entry ([#26269](https://github.com/frappe/erpnext/pull/26269))
+- Filter Cost Center and Project drop-down lists by Company ([#26045](https://github.com/frappe/erpnext/pull/26045))
+- Website item group logic for product listing in Item Group pages ([#26170](https://github.com/frappe/erpnext/pull/26170))
+- Chart not visible for First Response Time reports ([#26032](https://github.com/frappe/erpnext/pull/26032))
+- Incorrect billed qty in Sales Order analytics ([#26095](https://github.com/frappe/erpnext/pull/26095))
+- Material request and supplier quotation not linked if supplier quotation created from supplier portal ([#26023](https://github.com/frappe/erpnext/pull/26023))
+- Update leave allocation after submit ([#26191](https://github.com/frappe/erpnext/pull/26191))
+- Taxes on Internal Transfer payment entry ([#26188](https://github.com/frappe/erpnext/pull/26188))
+- Precision rate for packed items (bp #26046) ([#26217](https://github.com/frappe/erpnext/pull/26217))
+- Fixed rounding off ordered percent to 100 in condition ([#26152](https://github.com/frappe/erpnext/pull/26152))
+- Sanctioned loan amount limit check ([#26108](https://github.com/frappe/erpnext/pull/26108))
+- Purchase receipt gl entries with same item code ([#26202](https://github.com/frappe/erpnext/pull/26202))
+- Taxable value for invoices with additional discount ([#25906](https://github.com/frappe/erpnext/pull/25906))
+- Correct South Africa VAT Rate (Updated) ([#25894](https://github.com/frappe/erpnext/pull/25894))
+- Remove response_by and resolution_by if sla is removed ([#25997](https://github.com/frappe/erpnext/pull/25997))
+- POS loyalty card alignment ([#26051](https://github.com/frappe/erpnext/pull/26051))
+- Flaky test for Report Subcontracted Raw materials to be transferred ([#26043](https://github.com/frappe/erpnext/pull/26043))
+- Export invoices not visible in GSTR-1 report ([#26143](https://github.com/frappe/erpnext/pull/26143))
+- Account filter not working with accounting dimension filter ([#26211](https://github.com/frappe/erpnext/pull/26211))
+- Allow to select group warehouse while downloading materials from production plan ([#26126](https://github.com/frappe/erpnext/pull/26126))
+- Added freeze when trying to stop work order ([#26192](https://github.com/frappe/erpnext/pull/26192))
+- Time out while submit / cancel the stock transactions with more than 50 Items ([#26081](https://github.com/frappe/erpnext/pull/26081))
+- Address Card issues in e-commerce ([#26187](https://github.com/frappe/erpnext/pull/26187))
+- Error while booking deferred revenue ([#26195](https://github.com/frappe/erpnext/pull/26195))
+- Eliminate repeat creation of HSN codes ([#25947](https://github.com/frappe/erpnext/pull/25947))
+- Opening invoices can alter profit and loss of a closed year ([#25951](https://github.com/frappe/erpnext/pull/25951))
+- Payroll entry employee detail issue ([#25968](https://github.com/frappe/erpnext/pull/25968))
+- Auto tax calculations in Payment Entry ([#26037](https://github.com/frappe/erpnext/pull/26037))
+- Use pos invoice item name as unique identifier ([#26198](https://github.com/frappe/erpnext/pull/26198))
+- Billing address not fetched in Purchase Invoice ([#26100](https://github.com/frappe/erpnext/pull/26100))
+- Timeout while cancelling stock reconciliation ([#26098](https://github.com/frappe/erpnext/pull/26098))
+- Status indicator for delivery notes ([#26062](https://github.com/frappe/erpnext/pull/26062))
+- Unable to enter score in Assessment Result details grid ([#26031](https://github.com/frappe/erpnext/pull/26031))
+- Too many writes while renaming company abbreviation ([#26203](https://github.com/frappe/erpnext/pull/26203))
+- Chart not visible for First Response Time reports ([#26185](https://github.com/frappe/erpnext/pull/26185))
+- Job applicant link issue ([#25934](https://github.com/frappe/erpnext/pull/25934))
+- Fetch preferred shipping address (bp #26132) ([#26201](https://github.com/frappe/erpnext/pull/26201))
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 35097b9..2526e6d 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -11,7 +11,7 @@
 
 import erpnext
 from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map
-from erpnext.accounts.utils import check_if_stock_and_account_balance_synced, get_fiscal_year
+from erpnext.accounts.utils import get_fiscal_year
 from erpnext.controllers.accounts_controller import AccountsController
 from erpnext.stock import get_warehouse_account_map
 from erpnext.stock.stock_ledger import get_valuation_rate
@@ -356,42 +356,68 @@
 		}, update_modified)
 
 	def validate_inspection(self):
-		'''Checks if quality inspection is set for Items that require inspection.
-		On submit, throw an exception'''
-		inspection_required_fieldname = None
-		if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
-			inspection_required_fieldname = "inspection_required_before_purchase"
-		elif self.doctype in ["Delivery Note", "Sales Invoice"]:
-			inspection_required_fieldname = "inspection_required_before_delivery"
+		"""Checks if quality inspection is set/ is valid for Items that require inspection."""
+		inspection_fieldname_map = {
+			"Purchase Receipt": "inspection_required_before_purchase",
+			"Purchase Invoice": "inspection_required_before_purchase",
+			"Sales Invoice": "inspection_required_before_delivery",
+			"Delivery Note": "inspection_required_before_delivery"
+		}
+		inspection_required_fieldname = inspection_fieldname_map.get(self.doctype)
 
+		# return if inspection is not required on document level
 		if ((not inspection_required_fieldname and self.doctype != "Stock Entry") or
 			(self.doctype == "Stock Entry" and not self.inspection_required) or
 			(self.doctype in ["Sales Invoice", "Purchase Invoice"] and not self.update_stock)):
 				return
 
-		for d in self.get('items'):
-			qa_required = False
-			if (inspection_required_fieldname and not d.quality_inspection and
-				frappe.db.get_value("Item", d.item_code, inspection_required_fieldname)):
-				qa_required = True
-			elif self.doctype == "Stock Entry" and not d.quality_inspection and d.t_warehouse:
-				qa_required = True
-			if self.docstatus == 1 and d.quality_inspection:
-				qa_doc = frappe.get_doc("Quality Inspection", d.quality_inspection)
-				if qa_doc.docstatus == 0:
-					link = frappe.utils.get_link_to_form('Quality Inspection', d.quality_inspection)
-					frappe.throw(_("Quality Inspection: {0} is not submitted for the item: {1} in row {2}").format(link, d.item_code, d.idx), QualityInspectionNotSubmittedError)
+		for row in self.get('items'):
+			qi_required = False
+			if (inspection_required_fieldname and frappe.db.get_value("Item", row.item_code, inspection_required_fieldname)):
+				qi_required = True
+			elif self.doctype == "Stock Entry" and row.t_warehouse:
+				qi_required = True # inward stock needs inspection
 
-				if qa_doc.status != 'Accepted':
-					frappe.throw(_("Row {0}: Quality Inspection rejected for item {1}")
-						.format(d.idx, d.item_code), QualityInspectionRejectedError)
-			elif qa_required :
-				action = frappe.get_doc('Stock Settings').action_if_quality_inspection_is_not_submitted
-				if self.docstatus==1 and action == 'Stop':
-					frappe.throw(_("Quality Inspection required for Item {0} to submit").format(frappe.bold(d.item_code)),
-						exc=QualityInspectionRequiredError)
-				else:
-					frappe.msgprint(_("Create Quality Inspection for Item {0}").format(frappe.bold(d.item_code)))
+			if qi_required: # validate row only if inspection is required on item level
+				self.validate_qi_presence(row)
+				if self.docstatus == 1:
+					self.validate_qi_submission(row)
+					self.validate_qi_rejection(row)
+
+	def validate_qi_presence(self, row):
+		"""Check if QI is present on row level. Warn on save and stop on submit if missing."""
+		if not row.quality_inspection:
+			msg = f"Row #{row.idx}: Quality Inspection is required for Item {frappe.bold(row.item_code)}"
+			if self.docstatus == 1:
+				frappe.throw(_(msg), title=_("Inspection Required"), exc=QualityInspectionRequiredError)
+			else:
+				frappe.msgprint(_(msg), title=_("Inspection Required"), indicator="blue")
+
+	def validate_qi_submission(self, row):
+		"""Check if QI is submitted on row level, during submission"""
+		action = frappe.db.get_single_value("Stock Settings", "action_if_quality_inspection_is_not_submitted")
+		qa_docstatus = frappe.db.get_value("Quality Inspection", row.quality_inspection, "docstatus")
+
+		if not qa_docstatus == 1:
+			link = frappe.utils.get_link_to_form('Quality Inspection', row.quality_inspection)
+			msg = f"Row #{row.idx}: Quality Inspection {link} is not submitted for the item: {row.item_code}"
+			if action == "Stop":
+				frappe.throw(_(msg), title=_("Inspection Submission"), exc=QualityInspectionNotSubmittedError)
+			else:
+				frappe.msgprint(_(msg), alert=True, indicator="orange")
+
+	def validate_qi_rejection(self, row):
+		"""Check if QI is rejected on row level, during submission"""
+		action = frappe.db.get_single_value("Stock Settings", "action_if_quality_inspection_is_rejected")
+		qa_status = frappe.db.get_value("Quality Inspection", row.quality_inspection, "status")
+
+		if qa_status == "Rejected":
+			link = frappe.utils.get_link_to_form('Quality Inspection', row.quality_inspection)
+			msg = f"Row #{row.idx}: Quality Inspection {link} was rejected for item {row.item_code}"
+			if action == "Stop":
+				frappe.throw(_(msg), title=_("Inspection Rejected"), exc=QualityInspectionRejectedError)
+			else:
+				frappe.msgprint(_(msg), alert=True, indicator="orange")
 
 	def update_blanket_order(self):
 		blanket_orders = list(set([d.blanket_order for d in self.items if d.blanket_order]))
@@ -497,9 +523,6 @@
 		})
 		if future_sle_exists(args):
 			create_repost_item_valuation_entry(args)
-		elif not is_reposting_pending():
-			check_if_stock_and_account_balance_synced(self.posting_date,
-				self.company, self.doctype, self.name)
 
 @frappe.whitelist()
 def make_quality_inspections(doctype, docname, items):
diff --git a/erpnext/crm/doctype/appointment/appointment.json b/erpnext/crm/doctype/appointment/appointment.json
index 8517dde..016b8ec 100644
--- a/erpnext/crm/doctype/appointment/appointment.json
+++ b/erpnext/crm/doctype/appointment/appointment.json
@@ -102,7 +102,7 @@
   }
  ],
  "links": [],
- "modified": "2020-01-28 16:16:45.447213",
+ "modified": "2021-06-30 12:09:14.228756",
  "modified_by": "Administrator",
  "module": "CRM",
  "name": "Appointment",
@@ -153,6 +153,18 @@
    "role": "Sales User",
    "share": 1,
    "write": 1
+  },
+  {
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "Employee",
+   "share": 1,
+   "write": 1
   }
  ],
  "quick_entry": 1,
diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py
index d1d0968..ce3de40 100644
--- a/erpnext/crm/doctype/lead/lead.py
+++ b/erpnext/crm/doctype/lead/lead.py
@@ -168,12 +168,13 @@
 		if self.phone:
 			contact.append("phone_nos", {
 				"phone": self.phone,
-				"is_primary": 1
+				"is_primary_phone": 1
 			})
 
 		if self.mobile_no:
 			contact.append("phone_nos", {
-				"phone": self.mobile_no
+				"phone": self.mobile_no,
+				"is_primary_mobile_no":1
 			})
 
 		contact.insert(ignore_permissions=True)
diff --git a/erpnext/education/utils.py b/erpnext/education/utils.py
index 9db8a4a..3070e6a 100644
--- a/erpnext/education/utils.py
+++ b/erpnext/education/utils.py
@@ -355,11 +355,11 @@
 	student = get_current_student()
 	course_enrollment = get_enrollment("course", course, student.name)
 	if not course_enrollment:
-		program_enrollment = get_enrollment('program', program, student.name)
+		program_enrollment = get_enrollment('program', program.name, student.name)
 		if not program_enrollment:
 			frappe.throw(_("You are not enrolled in program {0}").format(program))
 			return
-		return student.enroll_in_course(course_name=course, program_enrollment=get_enrollment('program', program, student.name))
+		return student.enroll_in_course(course_name=course, program_enrollment=get_enrollment('program', program.name, student.name))
 	else:
 		return frappe.get_doc('Course Enrollment', course_enrollment)
 
diff --git a/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py b/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py
index c93b788..33119d8 100644
--- a/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py
+++ b/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py
@@ -6,7 +6,7 @@
 import frappe
 import unittest
 import json
-from frappe.utils import getdate
+from frappe.utils import getdate, strip_html
 from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_patient
 
 class TestPatientHistorySettings(unittest.TestCase):
@@ -44,9 +44,9 @@
 		self.assertTrue(medical_rec)
 
 		medical_rec = frappe.get_doc("Patient Medical Record", medical_rec)
-		expected_subject = "<b>Date: </b>{0}<br><b>Rating: </b>3<br><b>Feedback: </b>Test Patient History Settings<br>".format(
+		expected_subject = "Date: {0}Rating: 3Feedback: Test Patient History Settings".format(
 			frappe.utils.format_date(getdate()))
-		self.assertEqual(medical_rec.subject, expected_subject)
+		self.assertEqual(strip_html(medical_rec.subject), expected_subject)
 		self.assertEqual(medical_rec.patient, patient)
 		self.assertEqual(medical_rec.communication_date, getdate())
 
@@ -101,4 +101,4 @@
 	}).insert()
 	doc.submit()
 
-	return doc
\ No newline at end of file
+	return doc
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 3da606b..ba10b58 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -158,6 +158,7 @@
 			"parents": [{"label": _("Material Request"), "route": "material-requests"}]
 		}
 	},
+	{"from_route": "/project", "to_route": "Project"}
 ]
 
 standard_portal_menu_items = [
diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.json b/erpnext/hr/doctype/leave_allocation/leave_allocation.json
index ae009ba..3a6539e 100644
--- a/erpnext/hr/doctype/leave_allocation/leave_allocation.json
+++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.json
@@ -110,7 +110,7 @@
    "label": "Allocation"
   },
   {
-    "allow_on_submit": 1,
+   "allow_on_submit": 1,
    "bold": 1,
    "fieldname": "new_leaves_allocated",
    "fieldtype": "Float",
diff --git a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py
index 49dd701..fdcd533 100644
--- a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py
+++ b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py
@@ -164,7 +164,6 @@
 		leave_allocation.cancel()
 		self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_allocation.name}))
 
-
 	def test_leave_addition_after_submit(self):
 		frappe.db.sql("delete from `tabLeave Allocation`")
 		frappe.db.sql("delete from `tabLeave Ledger Entry`")
@@ -179,7 +178,6 @@
 	def test_leave_subtraction_after_submit(self):
 		frappe.db.sql("delete from `tabLeave Allocation`")
 		frappe.db.sql("delete from `tabLeave Ledger Entry`")
-
 		leave_allocation = create_leave_allocation()
 		leave_allocation.submit()
 		self.assertTrue(leave_allocation.total_leaves_allocated, 15)
@@ -199,8 +197,8 @@
 			"doctype": 'Leave Application',
 			"employee": employee.name,
 			"leave_type": "_Test Leave Type",
-			"from_date": nowdate(),
-			"to_date": add_days(nowdate(), 10),
+			"from_date": add_months(nowdate(), 2),
+			"to_date": add_months(add_days(nowdate(), 10), 2),
 			"company": erpnext.get_default_company() or "_Test Company",
 			"docstatus": 1,
 			"status": "Approved",
diff --git a/erpnext/hr/doctype/training_event/training_event.js b/erpnext/hr/doctype/training_event/training_event.js
index b7d34b1..d5f6e5f 100644
--- a/erpnext/hr/doctype/training_event/training_event.js
+++ b/erpnext/hr/doctype/training_event/training_event.js
@@ -20,11 +20,10 @@
 				frappe.set_route("List", "Training Feedback");
 			});
 		}
-	}
-});
+		frm.events.set_employee_query(frm);
+	},
 
-frappe.ui.form.on("Training Event Employee", {
-	employee: function (frm) {
+	set_employee_query: function(frm) {
 		let emp = [];
 		for (let d in frm.doc.employees) {
 			if (frm.doc.employees[d].employee) {
@@ -34,9 +33,17 @@
 		frm.set_query("employee", "employees", function () {
 			return {
 				filters: {
-					name: ["NOT IN", emp]
+					name: ["NOT IN", emp],
+					status: "Active"
 				}
 			};
 		});
 	}
 });
+
+frappe.ui.form.on("Training Event Employee", {
+	employee: function(frm) {
+		frm.events.set_employee_query(frm);
+	}
+});
+
diff --git a/erpnext/hr/doctype/training_event_employee/training_event_employee.json b/erpnext/hr/doctype/training_event_employee/training_event_employee.json
index 2d313e9..bcb7d5e 100644
--- a/erpnext/hr/doctype/training_event_employee/training_event_employee.json
+++ b/erpnext/hr/doctype/training_event_employee/training_event_employee.json
@@ -19,6 +19,7 @@
    "fieldtype": "Link",
    "in_list_view": 1,
    "label": "Employee",
+   "no_copy": 1,
    "options": "Employee"
   },
   {
@@ -68,7 +69,7 @@
  "index_web_pages_for_search": 1,
  "istable": 1,
  "links": [],
- "modified": "2021-05-21 12:41:59.336237",
+ "modified": "2021-07-02 17:20:27.630176",
  "modified_by": "Administrator",
  "module": "HR",
  "name": "Training Event Employee",
diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js
index 44f841f..c566688 100644
--- a/erpnext/manufacturing/doctype/bom/bom.js
+++ b/erpnext/manufacturing/doctype/bom/bom.js
@@ -71,7 +71,6 @@
 
 	refresh: function(frm) {
 		frm.toggle_enable("item", frm.doc.__islocal);
-		toggle_operations(frm);
 
 		frm.set_indicator_formatter('item_code',
 			function(doc) {
@@ -326,8 +325,7 @@
 			freeze: true,
 			args: {
 				update_parent: true,
-				from_child_bom:false,
-				save: frm.doc.docstatus === 1 ? true : false
+				from_child_bom:false
 			},
 			callback: function(r) {
 				refresh_field("items");
@@ -651,15 +649,8 @@
 	erpnext.bom.calculate_total(frm.doc);
 });
 
-var toggle_operations = function(frm) {
-	frm.toggle_display("operations_section", cint(frm.doc.with_operations) == 1);
-	frm.toggle_display("transfer_material_against", cint(frm.doc.with_operations) == 1);
-	frm.toggle_reqd("transfer_material_against", cint(frm.doc.with_operations) == 1);
-};
-
 frappe.ui.form.on("BOM", "with_operations", function(frm) {
 	if(!cint(frm.doc.with_operations)) {
 		frm.set_value("operations", []);
 	}
-	toggle_operations(frm);
 });
diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json
index f551b91..f38d1b9 100644
--- a/erpnext/manufacturing/doctype/bom/bom.json
+++ b/erpnext/manufacturing/doctype/bom/bom.json
@@ -193,6 +193,7 @@
   },
   {
    "default": "Work Order",
+   "depends_on": "with_operations",
    "fieldname": "transfer_material_against",
    "fieldtype": "Select",
    "label": "Transfer Material Against",
@@ -235,6 +236,7 @@
   {
    "fieldname": "operations_section",
    "fieldtype": "Section Break",
+   "hide_border": 1,
    "oldfieldtype": "Section Break"
   },
   {
@@ -245,6 +247,7 @@
    "options": "Routing"
   },
   {
+   "depends_on": "with_operations",
    "fieldname": "operations",
    "fieldtype": "Table",
    "label": "Operations",
@@ -517,7 +520,7 @@
  "image_field": "image",
  "is_submittable": 1,
  "links": [],
- "modified": "2020-05-21 12:29:32.634952",
+ "modified": "2021-03-16 12:25:09.081968",
  "modified_by": "Administrator",
  "module": "Manufacturing",
  "name": "BOM",
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 9bb4028..3bd1fe6 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -662,7 +662,7 @@
 			self.get_routing()
 
 	def validate_operations(self):
-		if self.with_operations and not self.get('operations'):
+		if self.with_operations and not self.get('operations') and self.docstatus == 1:
 			frappe.throw(_("Operations cannot be left blank"))
 
 		if self.with_operations:
@@ -1100,6 +1100,8 @@
 		},
 		'BOM Item': {
 			'doctype': 'BOM Item',
+			# stop get_mapped_doc copying parent bom_no to children
+			'field_no_map': ['bom_no'],
 			'condition': lambda doc: doc.has_variants == 0
 		},
 	}, target_doc, postprocess)
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index 57a5458..c89f7d6 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -8,6 +8,7 @@
 from frappe.utils import cstr, flt
 from frappe.test_runner import make_test_records
 from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation
+from erpnext.manufacturing.doctype.bom.bom import make_variant_bom
 from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
 from erpnext.stock.doctype.item.test_item import make_item
 from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
@@ -248,6 +249,37 @@
 		for reqd_item, created_item in zip(reqd_order, created_order):
 			self.assertEqual(reqd_item, created_item.item_code)
 
+	def test_generated_variant_bom(self):
+		from erpnext.controllers.item_variant import create_variant
+
+		template_item = make_item(
+			"_TestTemplateItem", {"has_variants": 1, "attributes": [{"attribute": "Test Size"},]}
+		)
+		variant = create_variant(template_item.item_code, {"Test Size": "Large"})
+		variant.insert(ignore_if_duplicate=True)
+
+		bom_tree = {
+			template_item.item_code: {
+				"SubAssembly1": {"ChildPart1": {}, "ChildPart2": {},},
+				"ChildPart5": {},
+			}
+		}
+		template_bom = create_nested_bom(bom_tree, prefix="")
+		variant_bom = make_variant_bom(
+			template_bom.name, template_bom.name, variant.item_code, variant_items=[]
+		)
+		variant_bom.save()
+
+		reqd_order = template_bom.get_tree_representation().level_order_traversal()
+		created_order = variant_bom.get_tree_representation().level_order_traversal()
+
+		self.assertEqual(len(reqd_order), len(created_order))
+
+		for reqd_item, created_item in zip(reqd_order, created_order):
+			self.assertEqual(reqd_item.item_code, created_item.item_code)
+			self.assertEqual(reqd_item.qty, created_item.qty)
+			self.assertEqual(reqd_item.exploded_qty, created_item.exploded_qty)
+
 
 def get_default_bom(item_code="_Test FG Item 2"):
 	return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})
diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json
index 07464e3..4458e6d 100644
--- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json
+++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json
@@ -13,10 +13,10 @@
   "col_break1",
   "hour_rate",
   "time_in_mins",
-  "batch_size",
   "operating_cost",
   "base_hour_rate",
   "base_operating_cost",
+  "batch_size",
   "image"
  ],
  "fields": [
@@ -61,6 +61,8 @@
   },
   {
    "description": "In minutes",
+   "fetch_from": "operation.total_operation_time",
+   "fetch_if_empty": 1,
    "fieldname": "time_in_mins",
    "fieldtype": "Float",
    "in_list_view": 1,
@@ -104,7 +106,8 @@
    "label": "Image"
   },
   {
-   "default": "1",
+   "fetch_from": "operation.batch_size",
+   "fetch_if_empty": 1,
    "fieldname": "batch_size",
    "fieldtype": "Int",
    "label": "Batch Size"
@@ -120,7 +123,7 @@
  "index_web_pages_for_search": 1,
  "istable": 1,
  "links": [],
- "modified": "2020-10-13 18:14:10.018774",
+ "modified": "2021-01-12 14:48:09.596843",
  "modified_by": "Administrator",
  "module": "Manufacturing",
  "name": "BOM Operation",
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.js b/erpnext/manufacturing/doctype/job_card/job_card.js
index 4e8dd41..81860c9 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.js
+++ b/erpnext/manufacturing/doctype/job_card/job_card.js
@@ -11,6 +11,16 @@
 				}
 			};
 		});
+
+		frm.set_indicator_formatter('sub_operation',
+			function(doc) {
+				if (doc.status == "Pending") {
+					return "red";
+				} else {
+					return doc.status === "Complete" ? "green" : "orange";
+				}
+			}
+		);
 	},
 
 	refresh: function(frm) {
@@ -31,6 +41,10 @@
 			}
 		}
 
+		if (frm.doc.docstatus == 1 && !frm.doc.is_corrective_job_card) {
+			frm.trigger('setup_corrective_job_card');
+		}
+
 		frm.set_query("quality_inspection", function() {
 			return {
 				query: "erpnext.stock.doctype.quality_inspection.quality_inspection.quality_inspection_query",
@@ -43,12 +57,62 @@
 
 		frm.trigger("toggle_operation_number");
 
-		if (frm.doc.docstatus == 0 && (frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity)
+		if (frm.doc.docstatus == 0 && !frm.is_new() &&
+			(frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity)
 			&& (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) {
 			frm.trigger("prepare_timer_buttons");
 		}
 	},
 
+	setup_corrective_job_card: function(frm) {
+		frm.add_custom_button(__('Corrective Job Card'), () => {
+			let operations = frm.doc.sub_operations.map(d => d.sub_operation).concat(frm.doc.operation);
+
+			let fields = [
+				{
+					fieldtype: 'Link', label: __('Corrective Operation'), options: 'Operation',
+					fieldname: 'operation', get_query() {
+						return {
+							filters: {
+								"is_corrective_operation": 1
+							}
+						};
+					}
+				}, {
+					fieldtype: 'Link', label: __('For Operation'), options: 'Operation',
+					fieldname: 'for_operation', get_query() {
+						return {
+							filters: {
+								"name": ["in", operations]
+							}
+						};
+					}
+				}
+			];
+
+			frappe.prompt(fields, d => {
+				frm.events.make_corrective_job_card(frm, d.operation, d.for_operation);
+			}, __("Select Corrective Operation"));
+		}, __('Make'));
+	},
+
+	make_corrective_job_card: function(frm, operation, for_operation) {
+		frappe.call({
+			method: 'erpnext.manufacturing.doctype.job_card.job_card.make_corrective_job_card',
+			args: {
+				source_name: frm.doc.name,
+				operation: operation,
+				for_operation: for_operation
+			},
+			callback: function(r) {
+				if (r.message) {
+					frappe.model.sync(r.message);
+					frappe.set_route("Form", r.message.doctype, r.message.name);
+				}
+			}
+		});
+	},
+
 	operation: function(frm) {
 		frm.trigger("toggle_operation_number");
 
@@ -97,101 +161,105 @@
 
 	prepare_timer_buttons: function(frm) {
 		frm.trigger("make_dashboard");
-		if (!frm.doc.job_started) {
-			frm.add_custom_button(__("Start"), () => {
-				if (!frm.doc.employee) {
-					frappe.prompt({fieldtype: 'Link', label: __('Employee'), options: "Employee",
-						fieldname: 'employee'}, d => {
-						if (d.employee) {
-							frm.set_value("employee", d.employee);
-						} else {
-							frm.events.start_job(frm);
-						}
-					}, __("Enter Value"), __("Start"));
+
+		if (!frm.doc.started_time && !frm.doc.current_time) {
+			frm.add_custom_button(__("Start Job"), () => {
+				if ((frm.doc.employee && !frm.doc.employee.length) || !frm.doc.employee) {
+					frappe.prompt({fieldtype: 'Table MultiSelect', label: __('Select Employees'),
+						options: "Job Card Time Log", fieldname: 'employees'}, d => {
+						frm.events.start_job(frm, "Work In Progress", d.employees);
+					}, __("Assign Job to Employee"));
 				} else {
-					frm.events.start_job(frm);
+					frm.events.start_job(frm, "Work In Progress", frm.doc.employee);
 				}
 			}).addClass("btn-primary");
 		} else if (frm.doc.status == "On Hold") {
-			frm.add_custom_button(__("Resume"), () => {
-				frappe.flags.resume_job = 1;
-				frm.events.start_job(frm);
+			frm.add_custom_button(__("Resume Job"), () => {
+				frm.events.start_job(frm, "Resume Job", frm.doc.employee);
 			}).addClass("btn-primary");
 		} else {
-			frm.add_custom_button(__("Pause"), () => {
-				frappe.flags.pause_job = 1;
-				frm.set_value("status", "On Hold");
-				frm.events.complete_job(frm);
+			frm.add_custom_button(__("Pause Job"), () => {
+				frm.events.complete_job(frm, "On Hold");
 			});
 
-			frm.add_custom_button(__("Complete"), () => {
-				let completed_time = frappe.datetime.now_datetime();
-				frm.trigger("hide_timer");
+			frm.add_custom_button(__("Complete Job"), () => {
+				var sub_operations = frm.doc.sub_operations;
 
-				if (frm.doc.for_quantity) {
+				let set_qty = true;
+				if (sub_operations && sub_operations.length > 1) {
+					set_qty = false;
+					let last_op_row = sub_operations[sub_operations.length - 2];
+
+					if (last_op_row.status == 'Complete') {
+						set_qty = true;
+					}
+				}
+
+				if (set_qty) {
 					frappe.prompt({fieldtype: 'Float', label: __('Completed Quantity'),
-						fieldname: 'qty', reqd: 1, default: frm.doc.for_quantity}, data => {
-							frm.events.complete_job(frm, completed_time, data.qty);
-						}, __("Enter Value"), __("Complete"));
+						fieldname: 'qty', default: frm.doc.for_quantity}, data => {
+						frm.events.complete_job(frm, "Complete", data.qty);
+					}, __("Enter Value"));
 				} else {
-					frm.events.complete_job(frm, completed_time, 0);
+					frm.events.complete_job(frm, "Complete", 0.0);
 				}
 			}).addClass("btn-primary");
 		}
 	},
 
-	start_job: function(frm) {
-		let row = frappe.model.add_child(frm.doc, 'Job Card Time Log', 'time_logs');
-		row.from_time = frappe.datetime.now_datetime();
-		frm.set_value('job_started', 1);
-		frm.set_value('started_time' , row.from_time);
-		frm.set_value("status", "Work In Progress");
-
-		if (!frappe.flags.resume_job) {
-			frm.set_value('current_time' , 0);
-		}
-
-		frm.save();
+	start_job: function(frm, status, employee) {
+		const args = {
+			job_card_id: frm.doc.name,
+			start_time: frappe.datetime.now_datetime(),
+			employees: employee,
+			status: status
+		};
+		frm.events.make_time_log(frm, args);
 	},
 
-	complete_job: function(frm, completed_time, completed_qty) {
-		frm.doc.time_logs.forEach(d => {
-			if (d.from_time && !d.to_time) {
-				d.to_time = completed_time || frappe.datetime.now_datetime();
-				d.completed_qty = completed_qty || 0;
+	complete_job: function(frm, status, completed_qty) {
+		const args = {
+			job_card_id: frm.doc.name,
+			complete_time: frappe.datetime.now_datetime(),
+			status: status,
+			completed_qty: completed_qty
+		};
+		frm.events.make_time_log(frm, args);
+	},
 
-				if(frappe.flags.pause_job) {
-					let currentIncrement = moment(d.to_time).diff(moment(d.from_time),"seconds") || 0;
-					frm.set_value('current_time' , currentIncrement + (frm.doc.current_time || 0));
-				} else {
-					frm.set_value('started_time' , '');
-					frm.set_value('job_started', 0);
-					frm.set_value('current_time' , 0);
-				}
+	make_time_log: function(frm, args) {
+		frm.events.update_sub_operation(frm, args);
 
-				frm.save();
+		frappe.call({
+			method: "erpnext.manufacturing.doctype.job_card.job_card.make_time_log",
+			args: {
+				args: args
+			},
+			freeze: true,
+			callback: function () {
+				frm.reload_doc();
+				frm.trigger("make_dashboard");
 			}
 		});
 	},
 
+	update_sub_operation: function(frm, args) {
+		if (frm.doc.sub_operations && frm.doc.sub_operations.length) {
+			let sub_operations = frm.doc.sub_operations.filter(d => d.status != 'Complete');
+			if (sub_operations && sub_operations.length) {
+				args["sub_operation"] = sub_operations[0].sub_operation;
+			}
+		}
+	},
+
 	validate: function(frm) {
 		if ((!frm.doc.time_logs || !frm.doc.time_logs.length) && frm.doc.started_time) {
 			frm.trigger("reset_timer");
 		}
 	},
 
-	employee: function(frm) {
-		if (frm.doc.job_started && !frm.doc.current_time) {
-			frm.trigger("reset_timer");
-		} else {
-			frm.events.start_job(frm);
-		}
-	},
-
 	reset_timer: function(frm) {
 		frm.set_value('started_time' , '');
-		frm.set_value('job_started', 0);
-		frm.set_value('current_time' , 0);
 	},
 
 	make_dashboard: function(frm) {
@@ -297,7 +365,6 @@
 	},
 
 	to_time: function(frm) {
-		frm.set_value('job_started', 0);
 		frm.set_value('started_time', '');
 	}
 })
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json
index 5713f69..046e2fd 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.json
+++ b/erpnext/manufacturing/doctype/job_card/job_card.json
@@ -9,38 +9,49 @@
   "naming_series",
   "work_order",
   "bom_no",
-  "workstation",
-  "operation",
-  "operation_row_number",
   "column_break_4",
   "posting_date",
   "company",
-  "remarks",
   "production_section",
   "production_item",
   "item_name",
   "for_quantity",
-  "quality_inspection",
-  "wip_warehouse",
+  "serial_no",
   "column_break_12",
-  "employee",
-  "employee_name",
-  "status",
+  "wip_warehouse",
+  "quality_inspection",
   "project",
+  "batch_no",
+  "operation_section_section",
+  "operation",
+  "operation_row_number",
+  "column_break_18",
+  "workstation",
+  "employee",
+  "section_break_21",
+  "sub_operations",
   "timing_detail",
   "time_logs",
   "section_break_13",
   "total_completed_qty",
-  "total_time_in_mins",
   "column_break_15",
+  "total_time_in_mins",
   "section_break_8",
   "items",
+  "corrective_operation_section",
+  "for_job_card",
+  "is_corrective_job_card",
+  "column_break_33",
+  "hour_rate",
+  "for_operation",
   "more_information",
   "operation_id",
   "sequence_id",
   "transferred_qty",
   "requested_qty",
+  "status",
   "column_break_20",
+  "remarks",
   "barcode",
   "job_started",
   "started_time",
@@ -118,13 +129,6 @@
    "label": "Timing Detail"
   },
   {
-   "fieldname": "employee",
-   "fieldtype": "Link",
-   "in_standard_filter": 1,
-   "label": "Employee",
-   "options": "Employee"
-  },
-  {
    "allow_bulk_edit": 1,
    "fieldname": "time_logs",
    "fieldtype": "Table",
@@ -133,9 +137,11 @@
   },
   {
    "fieldname": "section_break_13",
-   "fieldtype": "Section Break"
+   "fieldtype": "Section Break",
+   "hide_border": 1
   },
   {
+   "default": "0",
    "fieldname": "total_completed_qty",
    "fieldtype": "Float",
    "label": "Total Completed Qty",
@@ -160,8 +166,7 @@
    "fieldname": "items",
    "fieldtype": "Table",
    "label": "Items",
-   "options": "Job Card Item",
-   "read_only": 1
+   "options": "Job Card Item"
   },
   {
    "collapsible": 1,
@@ -251,12 +256,7 @@
    "reqd": 1
   },
   {
-   "fetch_from": "employee.employee_name",
-   "fieldname": "employee_name",
-   "fieldtype": "Read Only",
-   "label": "Employee Name"
-  },
-  {
+   "collapsible": 1,
    "fieldname": "production_section",
    "fieldtype": "Section Break",
    "label": "Production"
@@ -314,11 +314,89 @@
    "label": "Quality Inspection",
    "no_copy": 1,
    "options": "Quality Inspection"
+  },
+  {
+   "allow_bulk_edit": 1,
+   "fieldname": "sub_operations",
+   "fieldtype": "Table",
+   "label": "Sub Operations",
+   "options": "Job Card Operation",
+   "read_only": 1
+  },
+  {
+   "fieldname": "operation_section_section",
+   "fieldtype": "Section Break",
+   "label": "Operation Section"
+  },
+  {
+   "fieldname": "column_break_18",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "section_break_21",
+   "fieldtype": "Section Break",
+   "hide_border": 1
+  },
+  {
+   "depends_on": "is_corrective_job_card",
+   "fieldname": "hour_rate",
+   "fieldtype": "Currency",
+   "label": "Hour Rate"
+  },
+  {
+   "collapsible": 1,
+   "depends_on": "is_corrective_job_card",
+   "fieldname": "corrective_operation_section",
+   "fieldtype": "Section Break",
+   "label": "Corrective Operation"
+  },
+  {
+   "default": "0",
+   "fieldname": "is_corrective_job_card",
+   "fieldtype": "Check",
+   "label": "Is Corrective Job Card",
+   "read_only": 1
+  },
+  {
+   "fieldname": "column_break_33",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "for_job_card",
+   "fieldtype": "Link",
+   "label": "For Job Card",
+   "options": "Job Card",
+   "read_only": 1
+  },
+  {
+   "fetch_from": "for_job_card.operation",
+   "fetch_if_empty": 1,
+   "fieldname": "for_operation",
+   "fieldtype": "Link",
+   "label": "For Operation",
+   "options": "Operation"
+  },
+  {
+   "fieldname": "employee",
+   "fieldtype": "Table MultiSelect",
+   "label": "Employee",
+   "options": "Job Card Time Log"
+  },
+  {
+   "fieldname": "serial_no",
+   "fieldtype": "Small Text",
+   "label": "Serial No"
+  },
+  {
+   "fieldname": "batch_no",
+   "fieldtype": "Link",
+   "label": "Batch No",
+   "options": "Batch"
   }
  ],
  "is_submittable": 1,
  "links": [],
- "modified": "2020-11-19 18:26:50.531664",
+ "modified": "2021-03-16 15:59:32.766484",
  "modified_by": "Administrator",
  "module": "Manufacturing",
  "name": "Job Card",
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index cdc4518..7f8f2ef 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -5,11 +5,12 @@
 from __future__ import unicode_literals
 import frappe
 import datetime
+import json
 from frappe import _, bold
 from frappe.model.mapper import get_mapped_doc
 from frappe.model.document import Document
 from frappe.utils import (flt, cint, time_diff_in_hours, get_datetime, getdate,
-	get_time, add_to_date, time_diff, add_days, get_datetime_str, get_link_to_form)
+	get_time, add_to_date, time_diff, add_days, get_datetime_str, get_link_to_form, time_diff_in_seconds)
 
 from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations
 
@@ -25,10 +26,21 @@
 		self.set_status()
 		self.validate_operation_id()
 		self.validate_sequence_id()
+		self.get_sub_operations()
+		self.update_sub_operation_status()
+
+	def get_sub_operations(self):
+		if self.operation:
+			self.sub_operations = []
+			for row in frappe.get_all("Sub Operation",
+				filters = {"parent": self.operation}, fields=["operation", "idx"]):
+				row.status = "Pending"
+				row.sub_operation = row.operation
+				self.append("sub_operations", row)
 
 	def validate_time_logs(self):
-		self.total_completed_qty = 0.0
 		self.total_time_in_mins = 0.0
+		self.total_completed_qty = 0.0
 
 		if self.get('time_logs'):
 			for d in self.get('time_logs'):
@@ -44,11 +56,14 @@
 					d.time_in_mins = time_diff_in_hours(d.to_time, d.from_time) * 60
 					self.total_time_in_mins += d.time_in_mins
 
-				if d.completed_qty:
+				if d.completed_qty and not self.sub_operations:
 					self.total_completed_qty += d.completed_qty
 
 			self.total_completed_qty = flt(self.total_completed_qty, self.precision("total_completed_qty"))
 
+		for row in self.sub_operations:
+			self.total_completed_qty += row.completed_qty
+
 	def get_overlap_for(self, args, check_next_available_slot=False):
 		production_capacity = 1
 
@@ -57,7 +72,7 @@
 				self.workstation, 'production_capacity') or 1
 			validate_overlap_for = " and jc.workstation = %(workstation)s "
 
-		if self.employee:
+		if args.get("employee"):
 			# override capacity for employee
 			production_capacity = 1
 			validate_overlap_for = " and jc.employee = %(employee)s "
@@ -80,7 +95,7 @@
 				"to_time": args.to_time,
 				"name": args.name or "No Name",
 				"parent": args.parent or "No Name",
-				"employee": self.employee,
+				"employee": args.get("employee"),
 				"workstation": self.workstation
 			}, as_dict=True)
 
@@ -158,6 +173,100 @@
 			row.planned_start_time = datetime.datetime.combine(start_date,
 				get_time(workstation_doc.working_hours[0].start_time))
 
+	def add_time_log(self, args):
+		last_row = []
+		employees = args.employees
+		if isinstance(employees, str):
+			employees = json.loads(employees)
+
+		if self.time_logs and len(self.time_logs) > 0:
+			last_row = self.time_logs[-1]
+
+		self.reset_timer_value(args)
+		if last_row and args.get("complete_time"):
+			for row in self.time_logs:
+				if not row.to_time:
+					row.update({
+						"to_time": get_datetime(args.get("complete_time")),
+						"operation": args.get("sub_operation"),
+						"completed_qty": args.get("completed_qty") or 0.0
+					})
+		elif args.get("start_time"):
+			for name in employees:
+				self.append("time_logs", {
+					"from_time": get_datetime(args.get("start_time")),
+					"employee": name.get('employee'),
+					"operation": args.get("sub_operation"),
+					"completed_qty": 0.0
+				})
+
+		if not self.employee:
+			self.set_employees(employees)
+
+		if self.status == "On Hold":
+			self.current_time = time_diff_in_seconds(last_row.to_time, last_row.from_time)
+
+		self.save()
+
+	def set_employees(self, employees):
+		for name in employees:
+			self.append('employee', {
+				'employee': name.get('employee'),
+				'completed_qty': 0.0
+			})
+
+	def reset_timer_value(self, args):
+		self.started_time = None
+
+		if args.get("status") in ["Work In Progress", "Complete"]:
+			self.current_time = 0.0
+
+			if args.get("status") == "Work In Progress":
+				self.started_time = get_datetime(args.get("start_time"))
+
+		if args.get("status") == "Resume Job":
+			args["status"] = "Work In Progress"
+
+		if args.get("status"):
+			self.status = args.get("status")
+
+	def update_sub_operation_status(self):
+		if not (self.sub_operations and self.time_logs):
+			return
+
+		operation_wise_completed_time = {}
+		for time_log in self.time_logs:
+			if time_log.operation not in operation_wise_completed_time:
+				operation_wise_completed_time.setdefault(time_log.operation,
+					frappe._dict({"status": "Pending", "completed_qty":0.0, "completed_time": 0.0, "employee": []}))
+
+			op_row = operation_wise_completed_time[time_log.operation]
+			op_row.status = "Work In Progress" if not time_log.time_in_mins else "Complete"
+			if self.status == 'On Hold':
+				op_row.status = 'Pause'
+
+			op_row.employee.append(time_log.employee)
+			if time_log.time_in_mins:
+				op_row.completed_time += time_log.time_in_mins
+				op_row.completed_qty += time_log.completed_qty
+
+		for row in self.sub_operations:
+			operation_deatils = operation_wise_completed_time.get(row.sub_operation)
+			if operation_deatils:
+				if row.status != 'Complete':
+					row.status = operation_deatils.status
+
+				row.completed_time = operation_deatils.completed_time
+				if operation_deatils.employee:
+					row.completed_time = row.completed_time / len(set(operation_deatils.employee))
+
+					if operation_deatils.completed_qty:
+						row.completed_qty = operation_deatils.completed_qty / len(set(operation_deatils.employee))
+			else:
+				row.status = 'Pending'
+				row.completed_time = 0.0
+				row.completed_qty = 0.0
+
 	def update_time_logs(self, row):
 		self.append("time_logs", {
 			"from_time": row.planned_start_time,
@@ -182,15 +291,18 @@
 
 			if self.get('operation') == d.operation:
 				self.append('items', {
-					'item_code': d.item_code,
-					'source_warehouse': d.source_warehouse,
-					'uom': frappe.db.get_value("Item", d.item_code, 'stock_uom'),
-					'item_name': d.item_name,
-					'description': d.description,
-					'required_qty': (d.required_qty * flt(self.for_quantity)) / doc.qty
+					"item_code": d.item_code,
+					"source_warehouse": d.source_warehouse,
+					"uom": frappe.db.get_value("Item", d.item_code, 'stock_uom'),
+					"item_name": d.item_name,
+					"description": d.description,
+					"required_qty": (d.required_qty * flt(self.for_quantity)) / doc.qty,
+					"rate": d.rate,
+					"amount": d.amount
 				})
 
 	def on_submit(self):
+		self.validate_transfer_qty()
 		self.validate_job_card()
 		self.update_work_order()
 		self.set_transferred_qty()
@@ -199,7 +311,16 @@
 		self.update_work_order()
 		self.set_transferred_qty()
 
+	def validate_transfer_qty(self):
+		if self.items and self.transferred_qty < self.for_quantity:
+			frappe.throw(_('Materials needs to be transferred to the work in progress warehouse for the job card {0}')
+				.format(self.name))
+
 	def validate_job_card(self):
+		if self.work_order and frappe.get_cached_value('Work Order', self.work_order, 'status') == 'Stopped':
+			frappe.throw(_("Transaction not allowed against stopped Work Order {0}")
+				.format(get_link_to_form('Work Order', self.work_order)))
+
 		if not self.time_logs:
 			frappe.throw(_("Time logs are required for {0} {1}")
 				.format(bold("Job Card"), get_link_to_form("Job Card", self.name)))
@@ -215,6 +336,10 @@
 		if not self.work_order:
 			return
 
+		if self.is_corrective_job_card and not cint(frappe.db.get_single_value('Manufacturing Settings',
+			'add_corrective_operation_cost_in_finished_good_valuation')):
+			return
+
 		for_quantity, time_in_mins = 0, 0
 		from_time_list, to_time_list = [], []
 
@@ -225,10 +350,24 @@
 			time_in_mins = flt(data[0].time_in_mins)
 
 		wo = frappe.get_doc('Work Order', self.work_order)
-		if self.operation_id:
+
+		if self.is_corrective_job_card:
+			self.update_corrective_in_work_order(wo)
+
+		elif self.operation_id:
 			self.validate_produced_quantity(for_quantity, wo)
 			self.update_work_order_data(for_quantity, time_in_mins, wo)
 
+	def update_corrective_in_work_order(self, wo):
+		wo.corrective_operation_cost = 0.0
+		for row in frappe.get_all('Job Card', fields = ['total_time_in_mins', 'hour_rate'],
+			filters = {'is_corrective_job_card': 1, 'docstatus': 1, 'work_order': self.work_order}):
+			wo.corrective_operation_cost += flt(row.total_time_in_mins) * flt(row.hour_rate)
+
+		wo.calculate_operating_cost()
+		wo.flags.ignore_validate_update_after_submit = True
+		wo.save()
+
 	def validate_produced_quantity(self, for_quantity, wo):
 		if self.docstatus < 2: return
 
@@ -248,8 +387,8 @@
 					min(from_time) as start_time, max(to_time) as end_time
 				FROM `tabJob Card` jc, `tabJob Card Time Log` jctl
 				WHERE
-					jctl.parent = jc.name and jc.work_order = %s
-					and jc.operation_id = %s and jc.docstatus = 1
+					jctl.parent = jc.name and jc.work_order = %s and jc.operation_id = %s
+					and jc.docstatus = 1 and IFNULL(jc.is_corrective_job_card, 0) = 0
 			""", (self.work_order, self.operation_id), as_dict=1)
 
 		for data in wo.operations:
@@ -271,7 +410,8 @@
 	def get_current_operation_data(self):
 		return frappe.get_all('Job Card',
 			fields = ["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"],
-			filters = {"docstatus": 1, "work_order": self.work_order, "operation_id": self.operation_id})
+			filters = {"docstatus": 1, "work_order": self.work_order, "operation_id": self.operation_id,
+				"is_corrective_job_card": 0})
 
 	def set_transferred_qty_in_job_card(self, ste_doc):
 		for row in ste_doc.items:
@@ -354,7 +494,11 @@
 				.format(bold(self.operation), work_order), OperationMismatchError)
 
 	def validate_sequence_id(self):
-		if not (self.work_order and self.sequence_id): return
+		if self.is_corrective_job_card:
+			return
+
+		if not (self.work_order and self.sequence_id):
+			return
 
 		current_operation_qty = 0.0
 		data = self.get_current_operation_data()
@@ -376,6 +520,17 @@
 				frappe.throw(_("{0}, complete the operation {1} before the operation {2}.")
 					.format(message, bold(row.operation), bold(self.operation)), OperationSequenceError)
 
+
+@frappe.whitelist()
+def make_time_log(args):
+	if isinstance(args, str):
+		args = json.loads(args)
+
+	args = frappe._dict(args)
+	doc = frappe.get_doc("Job Card", args.job_card_id)
+	doc.validate_sequence_id()
+	doc.add_time_log(args)
+
 @frappe.whitelist()
 def get_operation_details(work_order, operation):
 	if work_order and operation:
@@ -511,3 +666,28 @@
 			events.append(job_card_data)
 
 	return events
+
+@frappe.whitelist()
+def make_corrective_job_card(source_name, operation=None, for_operation=None, target_doc=None):
+	def set_missing_values(source, target):
+		target.is_corrective_job_card = 1
+		target.operation = operation
+		target.for_operation = for_operation
+
+		target.set('time_logs', [])
+		target.set('employee', [])
+		target.set('items', [])
+		target.get_sub_operations()
+		target.get_required_items()
+		target.validate_time_logs()
+
+	doclist = get_mapped_doc("Job Card", source_name, {
+		"Job Card": {
+			"doctype": "Job Card",
+			"field_map": {
+				"name": "for_job_card",
+			},
+		}
+	}, target_doc, set_missing_values)
+
+	return doclist
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/job_card_item/job_card_item.json b/erpnext/manufacturing/doctype/job_card_item/job_card_item.json
index 100ef4c..d91530d 100644
--- a/erpnext/manufacturing/doctype/job_card_item/job_card_item.json
+++ b/erpnext/manufacturing/doctype/job_card_item/job_card_item.json
@@ -25,8 +25,7 @@
    "fieldtype": "Link",
    "in_list_view": 1,
    "label": "Item Code",
-   "options": "Item",
-   "read_only": 1
+   "options": "Item"
   },
   {
    "fieldname": "source_warehouse",
@@ -67,8 +66,7 @@
    "fieldname": "required_qty",
    "fieldtype": "Float",
    "in_list_view": 1,
-   "label": "Required Qty",
-   "read_only": 1
+   "label": "Required Qty"
   },
   {
    "fieldname": "column_break_9",
@@ -107,7 +105,7 @@
  "index_web_pages_for_search": 1,
  "istable": 1,
  "links": [],
- "modified": "2021-02-11 13:50:13.804108",
+ "modified": "2021-04-22 18:50:00.003444",
  "modified_by": "Administrator",
  "module": "Manufacturing",
  "name": "Job Card Item",
diff --git a/erpnext/manufacturing/doctype/job_card_operation/__init__.py b/erpnext/manufacturing/doctype/job_card_operation/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/manufacturing/doctype/job_card_operation/__init__.py
diff --git a/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.json b/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.json
new file mode 100644
index 0000000..9a8692b
--- /dev/null
+++ b/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.json
@@ -0,0 +1,59 @@
+{
+ "actions": [],
+ "creation": "2020-12-07 16:58:38.449041",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "sub_operation",
+  "completed_time",
+  "status",
+  "completed_qty"
+ ],
+ "fields": [
+  {
+   "default": "Pending",
+   "fieldname": "status",
+   "fieldtype": "Select",
+   "in_list_view": 1,
+   "label": "Status",
+   "options": "Complete\nPause\nPending\nWork In Progress",
+   "read_only": 1
+  },
+  {
+   "description": "In mins",
+   "fieldname": "completed_time",
+   "fieldtype": "Data",
+   "in_list_view": 1,
+   "label": "Completed Time",
+   "read_only": 1
+  },
+  {
+   "fieldname": "sub_operation",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "label": "Operation",
+   "options": "Operation",
+   "read_only": 1
+  },
+  {
+   "fieldname": "completed_qty",
+   "fieldtype": "Float",
+   "label": "Completed Qty",
+   "read_only": 1
+  }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-03-16 18:24:35.399593",
+ "modified_by": "Administrator",
+ "module": "Manufacturing",
+ "name": "Job Card Operation",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.py b/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.py
new file mode 100644
index 0000000..85d7298
--- /dev/null
+++ b/erpnext/manufacturing/doctype/job_card_operation/job_card_operation.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class JobCardOperation(Document):
+	pass
diff --git a/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json b/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json
index 9dd54dd..a7102d7 100644
--- a/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json
+++ b/erpnext/manufacturing/doctype/job_card_time_log/job_card_time_log.json
@@ -1,14 +1,17 @@
 {
+ "actions": [],
  "creation": "2019-03-08 23:56:43.187569",
  "doctype": "DocType",
  "editable_grid": 1,
  "engine": "InnoDB",
  "field_order": [
+  "employee",
   "from_time",
   "to_time",
   "column_break_2",
   "time_in_mins",
-  "completed_qty"
+  "completed_qty",
+  "operation"
  ],
  "fields": [
   {
@@ -41,10 +44,27 @@
    "in_list_view": 1,
    "label": "Completed Qty",
    "reqd": 1
+  },
+  {
+   "fieldname": "employee",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "label": "Employee",
+   "options": "Employee"
+  },
+  {
+   "fieldname": "operation",
+   "fieldtype": "Link",
+   "label": "Operation",
+   "no_copy": 1,
+   "options": "Operation",
+   "read_only": 1
   }
  ],
+ "index_web_pages_for_search": 1,
  "istable": 1,
- "modified": "2019-12-03 12:56:02.285448",
+ "links": [],
+ "modified": "2020-12-23 14:30:00.970916",
  "modified_by": "Administrator",
  "module": "Manufacturing",
  "name": "Job Card Time Log",
diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json
index b7634da..024f784 100644
--- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json
+++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json
@@ -26,7 +26,10 @@
   "column_break_16",
   "overproduction_percentage_for_work_order",
   "other_settings_section",
-  "update_bom_costs_automatically"
+  "update_bom_costs_automatically",
+  "add_corrective_operation_cost_in_finished_good_valuation",
+  "column_break_23",
+  "make_serial_no_batch_from_work_order"
  ],
  "fields": [
   {
@@ -155,13 +158,30 @@
   {
    "fieldname": "column_break_5",
    "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "column_break_23",
+   "fieldtype": "Column Break"
+  },
+  {
+   "default": "0",
+   "description": "System will automatically create the serial numbers / batch for the Finished Good on submission of work order",
+   "fieldname": "make_serial_no_batch_from_work_order",
+   "fieldtype": "Check",
+   "label": "Make Serial No / Batch from Work Order"
+  },
+  {
+   "default": "0",
+   "fieldname": "add_corrective_operation_cost_in_finished_good_valuation",
+   "fieldtype": "Check",
+   "label": "Add Corrective Operation Cost in Finished Good Valuation"
   }
  ],
  "icon": "icon-wrench",
  "index_web_pages_for_search": 1,
  "issingle": 1,
  "links": [],
- "modified": "2020-10-13 10:55:43.996581",
+ "modified": "2021-03-16 15:54:38.967341",
  "modified_by": "Administrator",
  "module": "Manufacturing",
  "name": "Manufacturing Settings",
@@ -178,4 +198,4 @@
  "sort_field": "modified",
  "sort_order": "DESC",
  "track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/operation/operation.js b/erpnext/manufacturing/doctype/operation/operation.js
index 5c2aba6..102b678 100644
--- a/erpnext/manufacturing/doctype/operation/operation.js
+++ b/erpnext/manufacturing/doctype/operation/operation.js
@@ -2,7 +2,13 @@
 // For license information, please see license.txt
 
 frappe.ui.form.on('Operation', {
-	refresh: function(frm) {
-
+	setup: function(frm) {
+		frm.set_query('operation', 'sub_operations', function() {
+			return {
+				filters: {
+					'name': ['not in', [frm.doc.name]]
+				}
+			};
+		});
 	}
-});
+});
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/operation/operation.json b/erpnext/manufacturing/doctype/operation/operation.json
index c231fba..10a97ed 100644
--- a/erpnext/manufacturing/doctype/operation/operation.json
+++ b/erpnext/manufacturing/doctype/operation/operation.json
@@ -1,167 +1,132 @@
 {
- "allow_copy": 0, 
- "allow_import": 1, 
- "allow_rename": 1, 
- "autoname": "Prompt", 
- "beta": 0, 
- "creation": "2014-11-07 16:20:30.683186", 
- "custom": 0, 
- "docstatus": 0, 
- "doctype": "DocType", 
- "document_type": "Setup", 
- "editable_grid": 0, 
- "engine": "InnoDB", 
+ "actions": [],
+ "allow_import": 1,
+ "allow_rename": 1,
+ "autoname": "Prompt",
+ "creation": "2014-11-07 16:20:30.683186",
+ "doctype": "DocType",
+ "document_type": "Setup",
+ "engine": "InnoDB",
+ "field_order": [
+  "workstation",
+  "data_2",
+  "is_corrective_operation",
+  "job_card_section",
+  "create_job_card_based_on_batch_size",
+  "column_break_6",
+  "batch_size",
+  "sub_operations_section",
+  "sub_operations",
+  "total_operation_time",
+  "section_break_4",
+  "description"
+ ],
  "fields": [
   {
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "workstation", 
-   "fieldtype": "Link", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_list_view": 1, 
-   "in_standard_filter": 1, 
-   "label": "Default Workstation", 
-   "length": 0, 
-   "no_copy": 0, 
-   "options": "Workstation", 
-   "permlevel": 0, 
-   "precision": "", 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 0, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 0, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "unique": 0
-  }, 
+   "fieldname": "workstation",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "in_standard_filter": 1,
+   "label": "Default Workstation",
+   "options": "Workstation"
+  },
   {
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "section_break_4", 
-   "fieldtype": "Section Break", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_list_view": 0, 
-   "in_standard_filter": 0, 
-   "length": 0, 
-   "no_copy": 0, 
-   "permlevel": 0, 
-   "precision": "", 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 0, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 0, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "unique": 0
-  }, 
+   "collapsible": 1,
+   "fieldname": "section_break_4",
+   "fieldtype": "Section Break",
+   "label": "Operation Description"
+  },
   {
-   "allow_on_submit": 0, 
-   "bold": 0, 
-   "collapsible": 0, 
-   "columns": 0, 
-   "fieldname": "description", 
-   "fieldtype": "Text", 
-   "hidden": 0, 
-   "ignore_user_permissions": 0, 
-   "ignore_xss_filter": 0, 
-   "in_filter": 0, 
-   "in_list_view": 0, 
-   "in_standard_filter": 0, 
-   "label": "Description", 
-   "length": 0, 
-   "no_copy": 0, 
-   "permlevel": 0, 
-   "precision": "", 
-   "print_hide": 0, 
-   "print_hide_if_no_value": 0, 
-   "read_only": 0, 
-   "remember_last_selected_value": 0, 
-   "report_hide": 0, 
-   "reqd": 0, 
-   "search_index": 0, 
-   "set_only_once": 0, 
-   "unique": 0
+   "fieldname": "description",
+   "fieldtype": "Text",
+   "label": "Description"
+  },
+  {
+   "collapsible": 1,
+   "fieldname": "sub_operations_section",
+   "fieldtype": "Section Break",
+   "label": "Sub Operations"
+  },
+  {
+   "fieldname": "sub_operations",
+   "fieldtype": "Table",
+   "options": "Sub Operation"
+  },
+  {
+   "description": "Time in mins.",
+   "fieldname": "total_operation_time",
+   "fieldtype": "Float",
+   "label": "Total Operation Time",
+   "read_only": 1
+  },
+  {
+   "fieldname": "data_2",
+   "fieldtype": "Column Break"
+  },
+  {
+   "default": "1",
+   "depends_on": "create_job_card_based_on_batch_size",
+   "fieldname": "batch_size",
+   "fieldtype": "Int",
+   "label": "Batch Size",
+   "mandatory_depends_on": "create_job_card_based_on_batch_size"
+  },
+  {
+   "default": "0",
+   "fieldname": "create_job_card_based_on_batch_size",
+   "fieldtype": "Check",
+   "label": "Create Job Card based on Batch Size"
+  },
+  {
+   "collapsible": 1,
+   "fieldname": "job_card_section",
+   "fieldtype": "Section Break",
+   "label": "Job Card"
+  },
+  {
+   "fieldname": "column_break_6",
+   "fieldtype": "Column Break"
+  },
+  {
+   "default": "0",
+   "fieldname": "is_corrective_operation",
+   "fieldtype": "Check",
+   "label": "Is Corrective Operation"
   }
- ], 
- "hide_heading": 0, 
- "hide_toolbar": 0, 
- "icon": "fa fa-wrench", 
- "idx": 0, 
- "image_view": 0, 
- "in_create": 0, 
-
- "is_submittable": 0, 
- "issingle": 0, 
- "istable": 0, 
- "max_attachments": 0, 
- "modified": "2016-11-07 05:28:27.462413", 
- "modified_by": "Administrator", 
- "module": "Manufacturing", 
- "name": "Operation", 
- "name_case": "", 
- "owner": "Administrator", 
+ ],
+ "icon": "fa fa-wrench",
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-01-12 15:09:23.593338",
+ "modified_by": "Administrator",
+ "module": "Manufacturing",
+ "name": "Operation",
+ "owner": "Administrator",
  "permissions": [
   {
-   "amend": 0, 
-   "apply_user_permissions": 0, 
-   "cancel": 0, 
-   "create": 1, 
-   "delete": 1, 
-   "email": 0, 
-   "export": 1, 
-   "if_owner": 0, 
-   "import": 1, 
-   "is_custom": 0, 
-   "permlevel": 0, 
-   "print": 0, 
-   "read": 1, 
-   "report": 0, 
-   "role": "Manufacturing User", 
-   "set_user_permissions": 0, 
-   "share": 1, 
-   "submit": 0, 
+   "create": 1,
+   "delete": 1,
+   "export": 1,
+   "import": 1,
+   "read": 1,
+   "role": "Manufacturing User",
+   "share": 1,
    "write": 1
-  }, 
+  },
   {
-   "amend": 0, 
-   "apply_user_permissions": 0, 
-   "cancel": 0, 
-   "create": 1, 
-   "delete": 1, 
-   "email": 0, 
-   "export": 1, 
-   "if_owner": 0, 
-   "import": 1, 
-   "is_custom": 0, 
-   "permlevel": 0, 
-   "print": 0, 
-   "read": 1, 
-   "report": 1, 
-   "role": "Manufacturing Manager", 
-   "set_user_permissions": 0, 
-   "share": 1, 
-   "submit": 0, 
+   "create": 1,
+   "delete": 1,
+   "export": 1,
+   "import": 1,
+   "read": 1,
+   "report": 1,
+   "role": "Manufacturing Manager",
+   "share": 1,
    "write": 1
   }
- ], 
- "quick_entry": 1, 
- "read_only": 0, 
- "read_only_onload": 0, 
- "sort_field": "modified", 
- "sort_order": "DESC", 
- "track_seen": 0
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
 }
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/operation/operation.py b/erpnext/manufacturing/doctype/operation/operation.py
index 69e8329..374f320 100644
--- a/erpnext/manufacturing/doctype/operation/operation.py
+++ b/erpnext/manufacturing/doctype/operation/operation.py
@@ -2,9 +2,34 @@
 # For license information, please see license.txt
 
 from __future__ import unicode_literals
+
+import frappe
+from frappe import _
 from frappe.model.document import Document
 
 class Operation(Document):
 	def validate(self):
 		if not self.description:
 			self.description = self.name
+
+		self.duplicate_sub_operation()
+		self.set_total_time()
+
+	def duplicate_sub_operation(self):
+		operation_list = []
+		for row in self.sub_operations:
+			if row.operation in operation_list:
+				frappe.throw(_("The operation {0} can not add multiple times")
+					.format(frappe.bold(row.operation)))
+
+			if self.name == row.operation:
+				frappe.throw(_("The operation {0} can not be the sub operation")
+					.format(frappe.bold(row.operation)))
+
+			operation_list.append(row.operation)
+
+	def set_total_time(self):
+		self.total_operation_time = 0.0
+
+		for row in self.sub_operations:
+			self.total_operation_time += row.time_in_mins
diff --git a/erpnext/manufacturing/doctype/sub_operation/__init__.py b/erpnext/manufacturing/doctype/sub_operation/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/manufacturing/doctype/sub_operation/__init__.py
diff --git a/erpnext/manufacturing/doctype/sub_operation/sub_operation.js b/erpnext/manufacturing/doctype/sub_operation/sub_operation.js
new file mode 100644
index 0000000..be9db6a
--- /dev/null
+++ b/erpnext/manufacturing/doctype/sub_operation/sub_operation.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Sub Operation', {
+	// refresh: function(frm) {
+
+	// }
+});
diff --git a/erpnext/manufacturing/doctype/sub_operation/sub_operation.json b/erpnext/manufacturing/doctype/sub_operation/sub_operation.json
new file mode 100644
index 0000000..f63d2b9
--- /dev/null
+++ b/erpnext/manufacturing/doctype/sub_operation/sub_operation.json
@@ -0,0 +1,51 @@
+{
+ "actions": [],
+ "creation": "2020-12-07 15:39:47.488519",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "operation",
+  "time_in_mins",
+  "column_break_5",
+  "description"
+ ],
+ "fields": [
+  {
+   "fieldname": "operation",
+   "fieldtype": "Link",
+   "in_list_view": 1,
+   "label": "Operation",
+   "options": "Operation"
+  },
+  {
+   "description": "Time in mins",
+   "fieldname": "time_in_mins",
+   "fieldtype": "Float",
+   "in_list_view": 1,
+   "label": "Operation Time"
+  },
+  {
+   "fieldname": "column_break_5",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "description",
+   "fieldtype": "Small Text",
+   "label": "Description"
+  }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2020-12-07 18:09:18.005578",
+ "modified_by": "Administrator",
+ "module": "Manufacturing",
+ "name": "Sub Operation",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/sub_operation/sub_operation.py b/erpnext/manufacturing/doctype/sub_operation/sub_operation.py
new file mode 100644
index 0000000..f4b2775
--- /dev/null
+++ b/erpnext/manufacturing/doctype/sub_operation/sub_operation.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class SubOperation(Document):
+	pass
diff --git a/erpnext/manufacturing/doctype/sub_operation/test_sub_operation.py b/erpnext/manufacturing/doctype/sub_operation/test_sub_operation.py
new file mode 100644
index 0000000..d3410ca
--- /dev/null
+++ b/erpnext/manufacturing/doctype/sub_operation/test_sub_operation.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestSubOperation(unittest.TestCase):
+	pass
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index cb1ee92..68de0b2 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -389,17 +389,12 @@
 		ste.submit()
 		stock_entries.append(ste)
 
-		job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name})
+		job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name}, order_by='creation asc')
 		self.assertEqual(len(job_cards), len(bom.operations))
 
 		for i, job_card in enumerate(job_cards):
 			doc = frappe.get_doc("Job Card", job_card)
-			doc.append("time_logs", {
-				"from_time": add_to_date(None, i),
-				"hours": 1,
-				"to_time": add_to_date(None, i + 1),
-				"completed_qty": doc.for_quantity
-			})
+			doc.time_logs[0].completed_qty = 1
 			doc.submit()
 
 		ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1))
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js
index 8088d93..5120485 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.js
+++ b/erpnext/manufacturing/doctype/work_order/work_order.js
@@ -141,8 +141,7 @@
 		}
 
 		if (frm.doc.docstatus === 1
-			&& frm.doc.operations && frm.doc.operations.length
-			&& frm.doc.qty != frm.doc.material_transferred_for_manufacturing) {
+			&& frm.doc.operations && frm.doc.operations.length) {
 
 			const not_completed = frm.doc.operations.filter(d => {
 				if(d.status != 'Completed') {
@@ -190,35 +189,41 @@
 		const dialog = frappe.prompt({fieldname: 'operations', fieldtype: 'Table', label: __('Operations'),
 			fields: [
 				{
-					fieldtype:'Link',
-					fieldname:'operation',
+					fieldtype: 'Link',
+					fieldname: 'operation',
 					label: __('Operation'),
-					read_only:1,
-					in_list_view:1
+					read_only: 1,
+					in_list_view: 1
 				},
 				{
-					fieldtype:'Link',
-					fieldname:'workstation',
+					fieldtype: 'Link',
+					fieldname: 'workstation',
 					label: __('Workstation'),
-					read_only:1,
-					in_list_view:1
+					read_only: 1,
+					in_list_view: 1
 				},
 				{
-					fieldtype:'Data',
-					fieldname:'name',
+					fieldtype: 'Data',
+					fieldname: 'name',
 					label: __('Operation Id')
 				},
 				{
-					fieldtype:'Float',
-					fieldname:'pending_qty',
+					fieldtype: 'Float',
+					fieldname: 'pending_qty',
 					label: __('Pending Qty'),
 				},
 				{
-					fieldtype:'Float',
-					fieldname:'qty',
+					fieldtype: 'Float',
+					fieldname: 'qty',
 					label: __('Quantity to Manufacture'),
-					read_only:0,
-					in_list_view:1,
+					read_only: 0,
+					in_list_view: 1,
+				},
+				{
+					fieldtype: 'Float',
+					fieldname: 'batch_size',
+					label: __('Batch Size'),
+					read_only: 1
 				},
 			],
 			data: operations_data,
@@ -229,9 +234,13 @@
 		}, function(data) {
 			frappe.call({
 				method: "erpnext.manufacturing.doctype.work_order.work_order.make_job_card",
+				freeze: true,
 				args: {
 					work_order: frm.doc.name,
 					operations: data.operations,
+				},
+				callback: function() {
+					frm.reload_doc();
 				}
 			});
 		}, __("Job Card"), __("Create"));
@@ -243,13 +252,16 @@
 			if(data.completed_qty != frm.doc.qty) {
 				pending_qty = frm.doc.qty - flt(data.completed_qty);
 
-				dialog.fields_dict.operations.df.data.push({
-					'name': data.name,
-					'operation': data.operation,
-					'workstation': data.workstation,
-					'qty': pending_qty,
-					'pending_qty': pending_qty,
-				});
+				if (pending_qty) {
+					dialog.fields_dict.operations.df.data.push({
+						'name': data.name,
+						'operation': data.operation,
+						'workstation': data.workstation,
+						'batch_size': data.batch_size,
+						'qty': pending_qty,
+						'pending_qty': pending_qty
+					});
+				}
 			}
 		});
 		dialog.fields_dict.operations.grid.refresh();
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json
index cd9edee..44d76d2 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.json
+++ b/erpnext/manufacturing/doctype/work_order/work_order.json
@@ -21,6 +21,12 @@
   "produced_qty",
   "sales_order",
   "project",
+  "serial_no_and_batch_for_finished_good_section",
+  "has_serial_no",
+  "has_batch_no",
+  "column_break_17",
+  "serial_no",
+  "batch_size",
   "settings_section",
   "allow_alternative_item",
   "use_multi_level_bom",
@@ -52,6 +58,7 @@
   "actual_operating_cost",
   "additional_operating_cost",
   "column_break_24",
+  "corrective_operation_cost",
   "total_operating_cost",
   "more_info",
   "description",
@@ -488,6 +495,57 @@
    "fieldtype": "Float",
    "label": "Lead Time",
    "read_only": 1
+  },
+  {
+   "collapsible": 1,
+   "depends_on": "eval:!doc.__islocal",
+   "fieldname": "serial_no_and_batch_for_finished_good_section",
+   "fieldtype": "Section Break",
+   "label": "Serial No and Batch for Finished Good"
+  },
+  {
+   "fieldname": "column_break_17",
+   "fieldtype": "Column Break"
+  },
+  {
+   "default": "0",
+   "fetch_from": "production_item.has_serial_no",
+   "fieldname": "has_serial_no",
+   "fieldtype": "Check",
+   "label": "Has Serial No",
+   "read_only": 1
+  },
+  {
+   "default": "0",
+   "fetch_from": "production_item.has_batch_no",
+   "fieldname": "has_batch_no",
+   "fieldtype": "Check",
+   "label": "Has Batch No",
+   "read_only": 1
+  },
+  {
+   "depends_on": "has_serial_no",
+   "fieldname": "serial_no",
+   "fieldtype": "Small Text",
+   "label": "Serial Nos",
+   "no_copy": 1
+  },
+  {
+   "default": "0",
+   "depends_on": "has_batch_no",
+   "fieldname": "batch_size",
+   "fieldtype": "Float",
+   "label": "Batch Size"
+  },
+  {
+   "allow_on_submit": 1,
+   "description": "From Corrective Job Card",
+   "fieldname": "corrective_operation_cost",
+   "fieldtype": "Currency",
+   "label": "Corrective Operation Cost",
+   "no_copy": 1,
+   "print_hide": 1,
+   "read_only": 1
   }
  ],
  "icon": "fa fa-cogs",
@@ -495,7 +553,7 @@
  "image_field": "image",
  "is_submittable": 1,
  "links": [],
- "modified": "2021-03-16 13:27:51.116484",
+ "modified": "2021-06-20 15:19:14.902699",
  "modified_by": "Administrator",
  "module": "Manufacturing",
  "name": "Work Order",
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index c06cf81..180815d 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -18,14 +18,16 @@
 from erpnext.stock.utils import get_bin, validate_warehouse_company, get_latest_stock_qty
 from erpnext.utilities.transaction_base import validate_uom_is_integer
 from frappe.model.mapper import get_mapped_doc
+from erpnext.stock.doctype.batch.batch import make_batch
+from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos, get_auto_serial_nos, auto_make_serial_nos
 
 class OverProductionError(frappe.ValidationError): pass
 class CapacityError(frappe.ValidationError): pass
 class StockOverProductionError(frappe.ValidationError): pass
 class OperationTooLongError(frappe.ValidationError): pass
 class ItemHasVariantError(frappe.ValidationError): pass
-
-from six import string_types
+class SerialNoQtyError(frappe.ValidationError):
+	pass
 
 
 class WorkOrder(Document):
@@ -123,7 +125,9 @@
 
 		variable_cost = self.actual_operating_cost if self.actual_operating_cost \
 			else self.planned_operating_cost
-		self.total_operating_cost = flt(self.additional_operating_cost) + flt(variable_cost)
+
+		self.total_operating_cost = (flt(self.additional_operating_cost)
+			+ flt(variable_cost) + flt(self.corrective_operation_cost))
 
 	def validate_work_order_against_so(self):
 		# already ordered qty
@@ -231,12 +235,15 @@
 
 		production_plan.run_method("update_produced_qty", produced_qty, self.production_plan_item)
 
+	def before_submit(self):
+		self.create_serial_no_batch_no()
+
 	def on_submit(self):
 		if not self.wip_warehouse:
 			frappe.throw(_("Work-in-Progress Warehouse is required before Submit"))
 		if not self.fg_warehouse:
 			frappe.throw(_("For Warehouse is required before Submit"))
-		
+
 		if self.production_plan and frappe.db.exists('Production Plan Item Reference',{'parent':self.production_plan}):
 			self.update_work_order_qty_in_combined_so()
 		else:
@@ -256,12 +263,76 @@
 			self.update_work_order_qty_in_combined_so()
 		else:
 			self.update_work_order_qty_in_so()
-			
+
 		self.delete_job_card()
 		self.update_completed_qty_in_material_request()
 		self.update_planned_qty()
 		self.update_ordered_qty()
 		self.update_reserved_qty_for_production()
+		self.delete_auto_created_batch_and_serial_no()
+
+	def create_serial_no_batch_no(self):
+		if not (self.has_serial_no or self.has_batch_no):
+			return
+
+		if not cint(frappe.db.get_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order")):
+			return
+
+		if self.has_batch_no:
+			self.create_batch_for_finished_good()
+
+		args = {
+			"item_code": self.production_item,
+			"work_order": self.name
+		}
+
+		if self.has_serial_no:
+			self.make_serial_nos(args)
+
+	def create_batch_for_finished_good(self):
+		total_qty = self.qty
+		if not self.batch_size:
+			self.batch_size = total_qty
+
+		while total_qty > 0:
+			qty = self.batch_size
+			if self.batch_size >= total_qty:
+				qty = total_qty
+
+			if total_qty > self.batch_size:
+				total_qty -= self.batch_size
+			else:
+				qty = total_qty
+				total_qty = 0
+
+			make_batch(frappe._dict({
+				"item": self.production_item,
+				"qty_to_produce": qty,
+				"reference_doctype": self.doctype,
+				"reference_name": self.name
+			}))
+
+	def delete_auto_created_batch_and_serial_no(self):
+		for row in frappe.get_all("Serial No", filters = {"work_order": self.name}):
+			frappe.delete_doc("Serial No", row.name)
+			self.db_set("serial_no", "")
+
+		for row in frappe.get_all("Batch", filters = {"reference_name": self.name}):
+			frappe.delete_doc("Batch", row.name)
+
+	def make_serial_nos(self, args):
+		serial_no_series = frappe.get_cached_value("Item", self.production_item, "serial_no_series")
+		if serial_no_series:
+			self.serial_no = get_auto_serial_nos(serial_no_series, self.qty)
+
+		if self.serial_no:
+			args.update({"serial_no": self.serial_no, "actual_qty": self.qty})
+			auto_make_serial_nos(args)
+
+		serial_nos_length = len(get_serial_nos(self.serial_no))
+		if serial_nos_length != self.qty:
+			frappe.throw(_("{0} Serial Numbers required for Item {1}. You have provided {2}.")
+				.format(self.qty, self.production_item, serial_nos_length), SerialNoQtyError)
 
 	def create_job_card(self):
 		manufacturing_settings_doc = frappe.get_doc("Manufacturing Settings")
@@ -269,32 +340,40 @@
 		enable_capacity_planning = not cint(manufacturing_settings_doc.disable_capacity_planning)
 		plan_days = cint(manufacturing_settings_doc.capacity_planning_for_days) or 30
 
-		for i, row in enumerate(self.operations):
-			self.set_operation_start_end_time(i, row)
-
-			if not row.workstation:
-				frappe.throw(_("Row {0}: select the workstation against the operation {1}")
-					.format(row.idx, row.operation))
-
-			original_start_time = row.planned_start_time
-			job_card_doc = create_job_card(self, row,
-				enable_capacity_planning=enable_capacity_planning, auto_create=True)
-
-			if enable_capacity_planning and job_card_doc:
-				row.planned_start_time = job_card_doc.time_logs[-1].from_time
-				row.planned_end_time = job_card_doc.time_logs[-1].to_time
-
-				if date_diff(row.planned_start_time, original_start_time) > plan_days:
-					frappe.message_log.pop()
-					frappe.throw(_("Unable to find the time slot in the next {0} days for the operation {1}.")
-						.format(plan_days, row.operation), CapacityError)
-
-				row.db_update()
+		for index, row in enumerate(self.operations):
+			qty = self.qty
+			while qty > 0:
+				qty = split_qty_based_on_batch_size(self, row, qty)
+				if row.job_card_qty > 0:
+					self.prepare_data_for_job_card(row, index,
+						plan_days, enable_capacity_planning)
 
 		planned_end_date = self.operations and self.operations[-1].planned_end_time
 		if planned_end_date:
 			self.db_set("planned_end_date", planned_end_date)
 
+	def prepare_data_for_job_card(self, row, index, plan_days, enable_capacity_planning):
+		self.set_operation_start_end_time(index, row)
+
+		if not row.workstation:
+			frappe.throw(_("Row {0}: select the workstation against the operation {1}")
+				.format(row.idx, row.operation))
+
+		original_start_time = row.planned_start_time
+		job_card_doc = create_job_card(self, row, auto_create=True,
+			enable_capacity_planning=enable_capacity_planning)
+
+		if enable_capacity_planning and job_card_doc:
+			row.planned_start_time = job_card_doc.time_logs[-1].from_time
+			row.planned_end_time = job_card_doc.time_logs[-1].to_time
+
+			if date_diff(row.planned_start_time, original_start_time) > plan_days:
+				frappe.message_log.pop()
+				frappe.throw(_("Unable to find the time slot in the next {0} days for the operation {1}.")
+					.format(plan_days, row.operation), CapacityError)
+
+			row.db_update()
+
 	def set_operation_start_end_time(self, idx, row):
 		"""Set start and end time for given operation. If first operation, set start as
 		`planned_start_date`, else add time diff to end time of earlier operation."""
@@ -361,7 +440,7 @@
 		work_order_qty = qty[0][0] if qty and qty[0][0] else 0
 		frappe.db.set_value('Sales Order Item',
 			self.sales_order_item, 'work_order_qty', flt(work_order_qty/total_bundle_qty, 2))
-		
+
 	def update_work_order_qty_in_combined_so(self):
 		total_bundle_qty = 1
 		if self.product_bundle_item:
@@ -374,7 +453,7 @@
 
 		prod_plan = frappe.get_doc('Production Plan', self.production_plan)
 		item_reference = frappe.get_value('Production Plan Item', self.production_plan_item, 'sales_order_item')
-		
+
 		for plan_reference in prod_plan.prod_plan_references:
 			work_order_qty = 0.0
 			if plan_reference.item_reference == item_reference:
@@ -382,7 +461,7 @@
 					work_order_qty = flt(plan_reference.qty) / total_bundle_qty
 				frappe.db.set_value('Sales Order Item',
 					plan_reference.sales_order_item, 'work_order_qty', work_order_qty)
-	
+
 	def update_completed_qty_in_material_request(self):
 		if self.material_request:
 			frappe.get_doc("Material Request", self.material_request).update_completed_qty([self.material_request_item])
@@ -666,6 +745,17 @@
 		bom.set_bom_material_details()
 		return bom
 
+	def update_batch_produced_qty(self, stock_entry_doc):
+		if not cint(frappe.db.get_single_value("Manufacturing Settings", "make_serial_no_batch_from_work_order")):
+			return
+
+		for row in stock_entry_doc.items:
+			if row.batch_no and (row.is_finished_item or row.is_scrap_item):
+				qty = frappe.get_all("Stock Entry Detail", filters = {"batch_no": row.batch_no, "docstatus": 1},
+					or_filters= {"is_finished_item": 1, "is_scrap_item": 1}, fields = ["sum(qty)"], as_list=1)[0][0]
+
+				frappe.db.set_value("Batch", row.batch_no, "produced_qty", flt(qty))
+
 @frappe.whitelist()
 @frappe.validate_and_sanitize_search_inputs
 def get_bom_operations(doctype, txt, searchfield, start, page_len, filters):
@@ -743,7 +833,7 @@
 	return wo_doc
 
 def add_variant_item(variant_items, wo_doc, bom_no, table_name="items"):
-	if isinstance(variant_items, string_types):
+	if isinstance(variant_items, str):
 		variant_items = json.loads(variant_items)
 
 	for item in variant_items:
@@ -823,6 +913,7 @@
 
 	stock_entry.set_stock_entry_type()
 	stock_entry.get_items()
+	stock_entry.set_serial_no_batch_for_finished_good()
 	return stock_entry.as_dict()
 
 @frappe.whitelist()
@@ -864,13 +955,47 @@
 
 @frappe.whitelist()
 def make_job_card(work_order, operations):
-	if isinstance(operations, string_types):
+	if isinstance(operations, str):
 		operations = json.loads(operations)
 
 	work_order = frappe.get_doc('Work Order', work_order)
 	for row in operations:
+		row = frappe._dict(row)
 		validate_operation_data(row)
-		create_job_card(work_order, row, row.get("qty"), auto_create=True)
+		qty = row.get("qty")
+		while qty > 0:
+			qty = split_qty_based_on_batch_size(work_order, row, qty)
+			if row.job_card_qty > 0:
+				create_job_card(work_order, row, auto_create=True)
+
+def split_qty_based_on_batch_size(wo_doc, row, qty):
+	if not cint(frappe.db.get_value("Operation",
+		row.operation, "create_job_card_based_on_batch_size")):
+		row.batch_size = row.get("qty") or wo_doc.qty
+
+	row.job_card_qty = row.batch_size
+	if row.batch_size and qty >= row.batch_size:
+		qty -= row.batch_size
+	elif qty > 0:
+		row.job_card_qty = qty
+		qty = 0
+
+	get_serial_nos_for_job_card(row, wo_doc)
+
+	return qty
+
+def get_serial_nos_for_job_card(row, wo_doc):
+	if not wo_doc.serial_no:
+		return
+
+	serial_nos = get_serial_nos(wo_doc.serial_no)
+	used_serial_nos = []
+	for d in frappe.get_all('Job Card', fields=['serial_no'],
+		filters={'docstatus': ('<', 2), 'work_order': wo_doc.name, 'operation_id': row.name}):
+		used_serial_nos.extend(get_serial_nos(d.serial_no))
+
+	serial_nos = sorted(list(set(serial_nos) - set(used_serial_nos)))
+	row.serial_no = '\n'.join(serial_nos[0:row.job_card_qty])
 
 def validate_operation_data(row):
 	if row.get("qty") <= 0:
@@ -889,20 +1014,22 @@
 			)
 		)
 
-def create_job_card(work_order, row, qty=0, enable_capacity_planning=False, auto_create=False):
+def create_job_card(work_order, row, enable_capacity_planning=False, auto_create=False):
 	doc = frappe.new_doc("Job Card")
 	doc.update({
 		'work_order': work_order.name,
 		'operation': row.get("operation"),
 		'workstation': row.get("workstation"),
 		'posting_date': nowdate(),
-		'for_quantity': qty or work_order.get('qty', 0),
+		'for_quantity': row.job_card_qty or work_order.get('qty', 0),
 		'operation_id': row.get("name"),
 		'bom_no': work_order.bom_no,
 		'project': work_order.project,
 		'company': work_order.company,
 		'sequence_id': row.get("sequence_id"),
-		'wip_warehouse': work_order.wip_warehouse
+		'wip_warehouse': work_order.wip_warehouse,
+		'hour_rate': row.get("hour_rate"),
+		'serial_no': row.get("serial_no")
 	})
 
 	if work_order.transfer_material_against == 'Job Card' and not work_order.skip_transfer:
diff --git a/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py b/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py
index 87c090f..9aa0715 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order_dashboard.py
@@ -4,10 +4,17 @@
 def get_data():
 	return {
 		'fieldname': 'work_order',
+		'non_standard_fieldnames': {
+			'Batch': 'reference_name'
+		},
 		'transactions': [
 			{
 				'label': _('Transactions'),
 				'items': ['Stock Entry', 'Job Card', 'Pick List']
+			},
+			{
+				'label': _('Reference'),
+				'items': ['Serial No', 'Batch']
 			}
 		]
 	}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json
index e28a42d..f7b8787 100644
--- a/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json
+++ b/erpnext/manufacturing/doctype/work_order_operation/work_order_operation.json
@@ -7,8 +7,9 @@
   "details",
   "operation",
   "bom",
-  "sequence_id",
+  "column_break_4",
   "description",
+  "sequence_id",
   "col_break1",
   "completed_qty",
   "status",
@@ -198,6 +199,10 @@
    "label": "Sequence ID",
    "print_hide": 1,
    "read_only": 1
+  },
+  {
+   "fieldname": "column_break_4",
+   "fieldtype": "Column Break"
   }
  ],
  "index_web_pages_for_search": 1,
diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/__init__.py b/erpnext/manufacturing/report/cost_of_poor_quality_report/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/__init__.py
diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js
new file mode 100644
index 0000000..97e7e0a
--- /dev/null
+++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.js
@@ -0,0 +1,105 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Cost of Poor Quality Report"] = {
+	"filters": [
+		{
+			label: __("Company"),
+			fieldname: "company",
+			fieldtype: "Link",
+			options: "Company",
+			default: frappe.defaults.get_user_default("Company"),
+			reqd: 1
+		},
+		{
+			label: __("From Date"),
+			fieldname:"from_date",
+			fieldtype: "Datetime",
+			default: frappe.datetime.convert_to_system_tz(frappe.datetime.add_months(frappe.datetime.now_datetime(), -1)),
+			reqd: 1
+		},
+		{
+			label: __("To Date"),
+			fieldname:"to_date",
+			fieldtype: "Datetime",
+			default: frappe.datetime.now_datetime(),
+			reqd: 1,
+		},
+		{
+			label: __("Job Card"),
+			fieldname: "name",
+			fieldtype: "Link",
+			options: "Job Card",
+			get_query: function() {
+				return {
+					filters: {
+						is_corrective_job_card: 1,
+						docstatus: 1
+					}
+				}
+			}
+		},
+		{
+			label: __("Work Order"),
+			fieldname: "work_order",
+			fieldtype: "Link",
+			options: "Work Order"
+		},
+		{
+			label: __("Operation"),
+			fieldname: "operation",
+			fieldtype: "Link",
+			options: "Operation",
+			get_query: function() {
+				return {
+					filters: {
+						is_corrective_operation: 1
+					}
+				}
+			}
+		},
+		{
+			label: __("Workstation"),
+			fieldname: "workstation",
+			fieldtype: "Link",
+			options: "Workstation"
+		},
+		{
+			label: __("Item"),
+			fieldname: "production_item",
+			fieldtype: "Link",
+			options: "Item"
+		},
+		{
+			label: __("Serial No"),
+			fieldname: "serial_no",
+			fieldtype: "Link",
+			options: "Serial No",
+			depends_on: "eval: doc.production_item",
+			get_query: function() {
+				var item_code = frappe.query_report.get_filter_value('production_item');
+				return {
+					filters: {
+						item_code: item_code
+					}
+				}
+			}
+		},
+		{
+			label: __("Batch No"),
+			fieldname: "batch_no",
+			fieldtype: "Link",
+			options: "Batch No",
+			depends_on: "eval: doc.production_item",
+			get_query: function() {
+				var item_code = frappe.query_report.get_filter_value('production_item');
+				return {
+					filters: {
+						item: item_code
+					}
+				}
+			}
+		},
+	]
+};
diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.json b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.json
new file mode 100644
index 0000000..ee63bc1
--- /dev/null
+++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.json
@@ -0,0 +1,33 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2021-01-11 11:10:58.292896",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "json": "{}",
+ "modified": "2021-01-11 11:11:03.594242",
+ "modified_by": "Administrator",
+ "module": "Manufacturing",
+ "name": "Cost of Poor Quality Report",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Job Card",
+ "report_name": "Cost of Poor Quality Report",
+ "report_type": "Script Report",
+ "roles": [
+  {
+   "role": "System Manager"
+  },
+  {
+   "role": "Manufacturing User"
+  },
+  {
+   "role": "Manufacturing Manager"
+  }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py
new file mode 100644
index 0000000..9f81e7d
--- /dev/null
+++ b/erpnext/manufacturing/report/cost_of_poor_quality_report/cost_of_poor_quality_report.py
@@ -0,0 +1,127 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe import _
+from frappe.utils import flt
+
+def execute(filters=None):
+	columns, data = [], []
+
+	columns = get_columns(filters)
+	data = get_data(filters)
+
+	return columns, data
+
+def get_data(report_filters):
+	data = []
+	operations = frappe.get_all("Operation", filters = {"is_corrective_operation": 1})
+	if operations:
+		operations = [d.name for d in operations]
+		fields = ["production_item as item_code", "item_name", "work_order", "operation",
+			"workstation", "total_time_in_mins", "name", "hour_rate", "serial_no", "batch_no"]
+
+		filters = get_filters(report_filters, operations)
+
+		job_cards = frappe.get_all("Job Card", fields = fields,
+			filters = filters)
+
+		for row in job_cards:
+			row.operating_cost = flt(row.hour_rate) * (flt(row.total_time_in_mins) / 60.0)
+			update_raw_material_cost(row, report_filters)
+			data.append(row)
+
+	return data
+
+def get_filters(report_filters, operations):
+	filters = {"docstatus": 1, "operation": ("in", operations), "is_corrective_job_card": 1}
+	for field in ["name", "work_order", "operation", "workstation", "company", "serial_no", "batch_no", "production_item"]:
+		if report_filters.get(field):
+			if field != 'serial_no':
+				filters[field] = report_filters.get(field)
+			else:
+				filters[field] = ('like', '% {} %'.format(report_filters.get(field)))
+
+	return filters
+
+def update_raw_material_cost(row, filters):
+	row.rm_cost = 0.0
+	for data in frappe.get_all("Job Card Item", fields = ["amount"],
+		filters={"parent": row.name, "docstatus": 1}):
+		row.rm_cost += data.amount
+
+def get_columns(filters):
+	return [
+		{
+			"label": _("Job Card"),
+			"fieldtype": "Link",
+			"fieldname": "name",
+			"options": "Job Card",
+			"width": "100"
+		},
+		{
+			"label": _("Work Order"),
+			"fieldtype": "Link",
+			"fieldname": "work_order",
+			"options": "Work Order",
+			"width": "100"
+		},
+		{
+			"label": _("Item Code"),
+			"fieldtype": "Link",
+			"fieldname": "item_code",
+			"options": "Item",
+			"width": "100"
+		},
+		{
+			"label": _("Item Name"),
+			"fieldtype": "Data",
+			"fieldname": "item_name",
+			"width": "100"
+		},
+		{
+			"label": _("Operation"),
+			"fieldtype": "Link",
+			"fieldname": "operation",
+			"options": "Operation",
+			"width": "100"
+		},
+		{
+			"label": _("Serial No"),
+			"fieldtype": "Data",
+			"fieldname": "serial_no",
+			"width": "100"
+		},
+		{
+			"label": _("Batch No"),
+			"fieldtype": "Data",
+			"fieldname": "batch_no",
+			"width": "100"
+		},
+		{
+			"label": _("Workstation"),
+			"fieldtype": "Link",
+			"fieldname": "workstation",
+			"options": "Workstation",
+			"width": "100"
+		},
+		{
+			"label": _("Operating Cost"),
+			"fieldtype": "Currency",
+			"fieldname": "operating_cost",
+			"width": "100"
+		},
+		{
+			"label": _("Raw Material Cost"),
+			"fieldtype": "Currency",
+			"fieldname": "rm_cost",
+			"width": "100"
+		},
+		{
+			"label": _("Total Time (in Mins)"),
+			"fieldtype": "Float",
+			"fieldname": "total_time_in_mins",
+			"width": "100"
+		}
+	]
\ No newline at end of file
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 339e7f9..986b0c5 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -290,3 +290,5 @@
 erpnext.patches.v13_0.set_training_event_attendance
 erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice
 erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold
+erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice
+erpnext.patches.v13_0.update_job_card_details
diff --git a/erpnext/patches/v13_0/update_job_card_details.py b/erpnext/patches/v13_0/update_job_card_details.py
new file mode 100644
index 0000000..d4e65c6
--- /dev/null
+++ b/erpnext/patches/v13_0/update_job_card_details.py
@@ -0,0 +1,16 @@
+# Copyright (c) 2019, Frappe and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+
+def execute():
+	frappe.reload_doc("manufacturing", "doctype", "job_card")
+	frappe.reload_doc("manufacturing", "doctype", "job_card_item")
+	frappe.reload_doc("manufacturing", "doctype", "work_order_operation")
+
+	frappe.db.sql(""" update `tabJob Card` jc, `tabWork Order Operation` wo
+		SET	jc.hour_rate =  wo.hour_rate
+		WHERE
+			jc.operation_id = wo.name and jc.docstatus < 2 and wo.hour_rate > 0
+	""")
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js
index f289260..496c37b 100644
--- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js
@@ -135,10 +135,26 @@
 		});
 
 		frm.set_query('employee', 'employees', () => {
-			if (!frm.doc.company) {
-				frappe.msgprint(__("Please set a Company"));
-				return [];
+			let error_fields = [];
+			let mandatory_fields = ['company', 'payroll_frequency', 'start_date', 'end_date'];
+
+			let message = __('Mandatory fields required in {0}', [__(frm.doc.doctype)]);
+
+			mandatory_fields.forEach(field => {
+				if (!frm.doc[field]) {
+					error_fields.push(frappe.unscrub(field));
+				}
+			});
+
+			if (error_fields && error_fields.length) {
+				message = message + '<br><br><ul><li>' + error_fields.join('</li><li>') + "</ul>";
+				frappe.throw({
+					message: message,
+					indicator: 'red',
+					title: __('Missing Fields')
+				});
 			}
+
 			return {
 				query: "erpnext.payroll.doctype.payroll_entry.payroll_entry.employee_query",
 				filters: frm.events.get_employee_filters(frm)
@@ -148,25 +164,22 @@
 
 	get_employee_filters: function (frm) {
 		let filters = {};
-		filters['company'] = frm.doc.company;
-		filters['start_date'] = frm.doc.start_date;
-		filters['end_date'] = frm.doc.end_date;
 		filters['salary_slip_based_on_timesheet'] = frm.doc.salary_slip_based_on_timesheet;
-		filters['payroll_frequency'] = frm.doc.payroll_frequency;
-		filters['payroll_payable_account'] = frm.doc.payroll_payable_account;
-		filters['currency'] = frm.doc.currency;
 
-		if (frm.doc.department) {
-			filters['department'] = frm.doc.department;
-		}
-		if (frm.doc.branch) {
-			filters['branch'] = frm.doc.branch;
-		}
-		if (frm.doc.designation) {
-			filters['designation'] = frm.doc.designation;
-		}
+		let fields = ['company', 'start_date', 'end_date', 'payroll_frequency', 'payroll_payable_account',
+			'currency', 'department', 'branch', 'designation'];
+
+		fields.forEach(field => {
+			if (frm.doc[field]) {
+				filters[field] = frm.doc[field];
+			}
+		});
+
 		if (frm.doc.employees) {
-			filters['employees'] = frm.doc.employees.filter(d => d.employee).map(d => d.employee);
+			let employees = frm.doc.employees.filter(d => d.employee).map(d => d.employee);
+			if (employees && employees.length) {
+				filters['employees'] = employees;
+			}
 		}
 		return filters;
 	},
diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
index 5c7c0a3..13cc423 100644
--- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
@@ -117,7 +117,6 @@
 			Creates salary slip for selected employees if already not created
 		"""
 		self.check_permission('write')
-		self.created = 1
 		employees = [emp.employee for emp in self.employees]
 		if employees:
 			args = frappe._dict({
@@ -680,9 +679,13 @@
 	conditions = []
 	include_employees = []
 	emp_cond = ''
+
+	if not filters.payroll_frequency:
+		frappe.throw(_('Select Payroll Frequency.'))
+
 	if filters.start_date and filters.end_date:
 		employee_list = get_employee_list(filters)
-		emp = filters.get('employees')
+		emp = filters.get('employees') or []
 		include_employees = [employee.employee for employee in employee_list if employee.employee not in emp]
 		filters.pop('start_date')
 		filters.pop('end_date')
diff --git a/erpnext/public/js/help_links.js b/erpnext/public/js/help_links.js
index aa9bba1..5c9a453 100644
--- a/erpnext/public/js/help_links.js
+++ b/erpnext/public/js/help_links.js
@@ -991,7 +991,7 @@
 		label: "Nested BOM Structure",
 		url:
 			docsUrl +
-			"user/manual/en/manufacturing/articles/nested-bom-structure",
+			"user/manual/en/manufacturing/articles/managing-multi-level-bom",
 	},
 ];
 
diff --git a/erpnext/regional/doctype/gst_settings/gst_settings.js b/erpnext/regional/doctype/gst_settings/gst_settings.js
index 808f9bc..cd682c5 100644
--- a/erpnext/regional/doctype/gst_settings/gst_settings.js
+++ b/erpnext/regional/doctype/gst_settings/gst_settings.js
@@ -35,6 +35,7 @@
 			return {
 				filters: {
 					company: row.company,
+					account_type: "Tax",
 					is_group: 0
 				}
 			};
diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js
index 23d4fe9..8ad30fa 100644
--- a/erpnext/regional/india/e_invoice/einvoice.js
+++ b/erpnext/regional/india/e_invoice/einvoice.js
@@ -1,6 +1,8 @@
 erpnext.setup_einvoice_actions = (doctype) => {
 	frappe.ui.form.on(doctype, {
 		async refresh(frm) {
+			if (frm.doc.docstatus == 2) return;
+
 			const res = await frappe.call({
 				method: 'erpnext.regional.india.e_invoice.utils.validate_eligibility',
 				args: { doc: frm.doc }
@@ -111,7 +113,7 @@
 
 			if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) {
 				const action = () => {
-					let message = __('Cancellation of e-way bill is currently not supported. ');
+					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.');
 
diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py
index 5d33c1b..81c7a6b 100644
--- a/erpnext/regional/india/e_invoice/utils.py
+++ b/erpnext/regional/india/e_invoice/utils.py
@@ -42,7 +42,10 @@
 	invalid_company = not frappe.db.get_value('E Invoice User', { 'company': doc.get('company') })
 	invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export']
 	company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin')
-	no_taxes_applied = not doc.get('taxes')
+
+	# if export invoice, then taxes can be empty
+	# invoice can only be ineligible if no taxes applied and is not an export invoice
+	no_taxes_applied = not doc.get('taxes') and not doc.get('gst_category') == 'Overseas'
 	has_non_gst_item = any(d for d in doc.get('items', []) if d.get('is_non_gst'))
 
 	if invalid_company or invalid_supply_type or company_transaction or no_taxes_applied or has_non_gst_item:
@@ -188,9 +191,10 @@
 
 		item.qty = abs(item.qty)
 
-		item.unit_rate = abs((abs(item.taxable_value) - item.discount_amount)/ item.qty)
-		item.gross_amount = abs(item.taxable_value) + item.discount_amount
+		item.unit_rate = abs(item.taxable_value / item.qty)
+		item.gross_amount = abs(item.taxable_value)
 		item.taxable_value = abs(item.taxable_value)
+		item.discount_amount = 0
 
 		item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None
 		item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None
diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py
index a4466e7..81c0918 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -834,8 +834,16 @@
 	depreciation_left = flt(row.total_number_of_depreciations) - flt(asset.number_of_depreciations_booked)
 
 	if row.depreciation_method in ("Straight Line", "Manual"):
-		depreciation_amount = (flt(row.value_after_depreciation) -
-			flt(row.expected_value_after_useful_life)) / depreciation_left
+		# if the Depreciation Schedule is being prepared for the first time
+		if not asset.flags.increase_in_asset_life:
+			depreciation_amount = (flt(row.value_after_depreciation) -
+				flt(row.expected_value_after_useful_life)) / depreciation_left
+
+		# if the Depreciation Schedule is being modified after Asset Repair
+		else:
+			depreciation_amount = (flt(row.value_after_depreciation) -
+				flt(row.expected_value_after_useful_life)) / (date_diff(asset.to_date, asset.available_for_use_date) / 365)
+		
 	else:
 		rate_of_depreciation = row.rate_of_depreciation
 		# if its the first depreciation
diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.py b/erpnext/selling/doctype/product_bundle/product_bundle.py
index d3281f7..ae3482f 100644
--- a/erpnext/selling/doctype/product_bundle/product_bundle.py
+++ b/erpnext/selling/doctype/product_bundle/product_bundle.py
@@ -4,6 +4,8 @@
 from __future__ import unicode_literals
 import frappe
 
+from frappe.utils import get_link_to_form
+
 from frappe import _
 
 from frappe.model.document import Document
@@ -18,6 +20,27 @@
 		from erpnext.utilities.transaction_base import validate_uom_is_integer
 		validate_uom_is_integer(self, "uom", "qty")
 
+	def on_trash(self):
+		linked_doctypes = ["Delivery Note", "Sales Invoice", "POS Invoice", "Purchase Receipt", "Purchase Invoice",
+			"Stock Entry", "Stock Reconciliation", "Sales Order", "Purchase Order", "Material Request"]
+
+		invoice_links = []
+		for doctype in linked_doctypes:
+			item_doctype = doctype + " Item"
+
+			if doctype == "Stock Entry":
+				item_doctype = doctype + " Detail"
+
+			invoices = frappe.db.get_all(item_doctype, {"item_code": self.new_item_code, "docstatus": 1}, ["parent"])
+
+			for invoice in invoices:
+				invoice_links.append(get_link_to_form(doctype, invoice['parent']))
+
+		if len(invoice_links):
+			frappe.throw(
+				"This Product Bundle is linked with {0}. You will have to cancel these documents in order to delete this Product Bundle"
+				.format(", ".join(invoice_links)), title=_("Not Allowed"))
+
 	def validate_main_item(self):
 		"""Validates, main Item is not a stock item"""
 		if frappe.db.get_value("Item", self.new_item_code, "is_stock_item"):
diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js
index 7cae0e4..38508c2 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_cart.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js
@@ -472,12 +472,7 @@
 		const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? frm.doc.grand_total : frm.doc.rounded_total;
 		this.render_grand_total(grand_total);
 
-		const taxes = frm.doc.taxes.map(t => {
-			return {
-				description: t.description, rate: t.rate
-			};
-		});
-		this.render_taxes(frm.doc.total_taxes_and_charges, taxes);
+		this.render_taxes(frm.doc.taxes);
 	}
 
 	render_net_total(value) {
@@ -502,14 +497,14 @@
 		);
 	}
 
-	render_taxes(value, taxes) {
+	render_taxes(taxes) {
 		if (taxes.length) {
 			const currency = this.events.get_frm().doc.currency;
 			const taxes_html = taxes.map(t => {
 				const description = /[0-9]+/.test(t.description) ? t.description : `${t.description} @ ${t.rate}%`;
 				return `<div class="tax-row">
 					<div class="tax-label">${description}</div>
-					<div class="tax-value">${format_currency(value, currency)}</div>
+					<div class="tax-value">${format_currency(t.tax_amount_after_discount_amount, currency)}</div>
 				</div>`;
 			}).join('');
 			this.$totals_section.find('.taxes-container').css('display', 'flex').html(taxes_html);
diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js
index c484873..f1a166b 100644
--- a/erpnext/selling/page/point_of_sale/pos_payment.js
+++ b/erpnext/selling/page/point_of_sale/pos_payment.js
@@ -56,7 +56,7 @@
 				);
 				let df_events = {
 					onchange: function() {
-						frm.set_value(this.df.fieldname, this.value);
+						frm.set_value(this.df.fieldname, this.get_value());
 					}
 				};
 				if (df.fieldtype == "Button") {
diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json
index 061986d..e6ec496 100644
--- a/erpnext/setup/doctype/company/company.json
+++ b/erpnext/setup/doctype/company/company.json
@@ -74,7 +74,7 @@
   "stock_received_but_not_billed",
   "service_received_but_not_billed",
   "expenses_included_in_valuation",
-  "fixed_asset_depreciation_settings",
+  "fixed_asset_defaults",
   "accumulated_depreciation_account",
   "depreciation_expense_account",
   "series_for_depreciation_entry",
@@ -83,6 +83,7 @@
   "disposal_account",
   "depreciation_cost_center",
   "capital_work_in_progress_account",
+  "repair_and_maintenance_account",
   "asset_received_but_not_billed",
   "budget_detail",
   "exception_budget_approver_role",
@@ -520,12 +521,6 @@
    "options": "Account"
   },
   {
-   "collapsible": 1,
-   "fieldname": "fixed_asset_depreciation_settings",
-   "fieldtype": "Section Break",
-   "label": "Fixed Asset Depreciation Settings"
-  },
-  {
    "fieldname": "accumulated_depreciation_account",
    "fieldtype": "Link",
    "label": "Accumulated Depreciation Account",
@@ -734,6 +729,18 @@
    "fieldtype": "Link",
    "label": "Default Payment Discount Account",
    "options": "Account"
+  },
+  {
+   "collapsible": 1,
+   "fieldname": "fixed_asset_defaults",
+   "fieldtype": "Section Break",
+   "label": "Fixed Asset Defaults"
+  },
+  {
+   "fieldname": "repair_and_maintenance_account",
+   "fieldtype": "Link",
+   "label": "Repair and Maintenance Account",
+   "options": "Account"
   }
  ],
  "icon": "fa fa-building",
@@ -741,7 +748,7 @@
  "image_field": "company_logo",
  "is_tree": 1,
  "links": [],
- "modified": "2021-05-07 03:11:28.189740",
+ "modified": "2021-05-12 16:51:08.187233",
  "modified_by": "Administrator",
  "module": "Setup",
  "name": "Company",
diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py
index 13269a8..d5dbd4c 100644
--- a/erpnext/setup/utils.py
+++ b/erpnext/setup/utils.py
@@ -28,21 +28,21 @@
 	from frappe.desk.page.setup_wizard.setup_wizard import setup_complete
 	if not frappe.get_list("Company"):
 		setup_complete({
-			"currency"			:"USD",
-			"full_name"			:"Test User",
-			"company_name"		:"Wind Power LLC",
-			"timezone"			:"America/New_York",
-			"company_abbr"		:"WP",
-			"industry"			:"Manufacturing",
-			"country"			:"United States",
-			"fy_start_date"		:"2011-01-01",
-			"fy_end_date"		:"2011-12-31",
-			"language"			:"english",
-			"company_tagline"	:"Testing",
-			"email"				:"test@erpnext.com",
-			"password"			:"test",
+			"currency"          :"USD",
+			"full_name"         :"Test User",
+			"company_name"      :"Wind Power LLC",
+			"timezone"          :"America/New_York",
+			"company_abbr"      :"WP",
+			"industry"          :"Manufacturing",
+			"country"           :"United States",
+			"fy_start_date"     :"2021-01-01",
+			"fy_end_date"       :"2021-12-31",
+			"language"          :"english",
+			"company_tagline"   :"Testing",
+			"email"             :"test@erpnext.com",
+			"password"          :"test",
 			"chart_of_accounts" : "Standard",
-			"domains"			: ["Manufacturing"],
+			"domains"           : ["Manufacturing"],
 		})
 
 	frappe.db.sql("delete from `tabLeave Allocation`")
diff --git a/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json
index 014f409..6ca3d63 100644
--- a/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json
+++ b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json
@@ -11,10 +11,11 @@
  "hide_custom": 0,
  "icon": "settings",
  "idx": 0,
+ "is_default": 0,
  "is_standard": 1,
  "label": "ERPNext Settings",
  "links": [],
- "modified": "2020-12-01 13:38:37.759596",
+ "modified": "2021-06-12 01:58:11.399566",
  "modified_by": "Administrator",
  "module": "Setup",
  "name": "ERPNext Settings",
@@ -109,6 +110,13 @@
    "label": "Domain Settings",
    "link_to": "Domain Settings",
    "type": "DocType"
+  },
+  {
+   "doc_view": "",
+   "icon": "retail",
+   "label": "Products Settings",
+   "link_to": "Products Settings",
+   "type": "DocType"
   }
  ]
-}
\ No newline at end of file
+}
diff --git a/erpnext/shopping_cart/product_query.py b/erpnext/shopping_cart/product_query.py
index 3eab4ff..6c92d96 100644
--- a/erpnext/shopping_cart/product_query.py
+++ b/erpnext/shopping_cart/product_query.py
@@ -87,7 +87,8 @@
 				filters=self.filters,
 				or_filters=self.or_filters,
 				start=start,
-				limit=self.page_length
+				limit=self.page_length,
+				order_by="weightage desc"
 			)
 
 		# Combine results having context of website item groups into item results
diff --git a/erpnext/stock/doctype/batch/batch.json b/erpnext/stock/doctype/batch/batch.json
index 943cb34..e6d2e13 100644
--- a/erpnext/stock/doctype/batch/batch.json
+++ b/erpnext/stock/doctype/batch/batch.json
@@ -1,4 +1,5 @@
 {
+ "actions": [],
  "allow_import": 1,
  "autoname": "field:batch_id",
  "creation": "2013-03-05 14:50:38",
@@ -25,7 +26,11 @@
   "reference_doctype",
   "reference_name",
   "section_break_7",
-  "description"
+  "description",
+  "manufacturing_section",
+  "qty_to_produce",
+  "column_break_23",
+  "produced_qty"
  ],
  "fields": [
   {
@@ -160,13 +165,35 @@
    "label": "Batch UOM",
    "options": "UOM",
    "read_only": 1
+  },
+  {
+   "fieldname": "manufacturing_section",
+   "fieldtype": "Section Break",
+   "label": "Manufacturing"
+  },
+  {
+   "fieldname": "qty_to_produce",
+   "fieldtype": "Float",
+   "label": "Qty To Produce",
+   "read_only": 1
+  },
+  {
+   "fieldname": "column_break_23",
+   "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "produced_qty",
+   "fieldtype": "Float",
+   "label": "Produced Qty",
+   "read_only": 1
   }
  ],
  "icon": "fa fa-archive",
  "idx": 1,
  "image_field": "image",
+ "links": [],
  "max_attachments": 5,
- "modified": "2020-09-18 17:26:09.703215",
+ "modified": "2021-01-07 11:10:09.149170",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Batch",
diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py
index 30bdbe8..b6eef6c 100644
--- a/erpnext/stock/doctype/batch/batch.py
+++ b/erpnext/stock/doctype/batch/batch.py
@@ -308,3 +308,8 @@
 	message = "Serial Nos" if len(serial_nos) > 1 else "Serial No"
 	frappe.throw(_("There is no batch found against the {0}: {1}")
 		.format(message, serial_no_link))
+
+def make_batch(args):
+	if frappe.db.get_value("Item", args.item, "has_batch_no"):
+		args.doctype = "Batch"
+		frappe.get_doc(args).insert().name
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index 45e3c21..568f0ef 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -93,7 +93,7 @@
 
 		erpnext.item.edit_prices_button(frm);
 		erpnext.item.toggle_attributes(frm);
-		
+
 		if (!frm.doc.is_fixed_asset) {
 			erpnext.item.make_dashboard(frm);
 		}
@@ -381,7 +381,8 @@
 		// Show Stock Levels only if is_stock_item
 		if (frm.doc.is_stock_item) {
 			frappe.require('item-dashboard.bundle.js', function() {
-				const section = frm.dashboard.add_section('', __("Stock Levels"));
+				frm.dashboard.parent.find('.stock-levels').remove();
+				const section = frm.dashboard.add_section('', __("Stock Levels"), 'stock-levels');
 				erpnext.item.item_dashboard = new erpnext.stock.ItemDashboard({
 					parent: section,
 					item_code: frm.doc.name,
diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
index 5df4d87..bf969f9 100644
--- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
+++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
@@ -41,7 +41,7 @@
 
 	def validate(self):
 		self.check_mandatory()
-		self.validate_purchase_receipts()
+		self.validate_receipt_documents()
 		init_landed_taxes_and_totals(self)
 		self.set_total_taxes_and_charges()
 		if not self.get("items"):
@@ -56,14 +56,23 @@
 			frappe.throw(_("Please enter Receipt Document"))
 
 
-	def validate_purchase_receipts(self):
+	def validate_receipt_documents(self):
 		receipt_documents = []
 
 		for d in self.get("purchase_receipts"):
-			if frappe.db.get_value(d.receipt_document_type, d.receipt_document, "docstatus") != 1:
-				frappe.throw(_("Receipt document must be submitted"))
-			else:
-				receipt_documents.append(d.receipt_document)
+			docstatus = frappe.db.get_value(d.receipt_document_type, d.receipt_document, "docstatus")
+			if docstatus != 1:
+				msg = f"Row {d.idx}: {d.receipt_document_type} {frappe.bold(d.receipt_document)} must be submitted"
+				frappe.throw(_(msg), title=_("Invalid Document"))
+
+			if d.receipt_document_type == "Purchase Invoice":
+				update_stock = frappe.db.get_value(d.receipt_document_type, d.receipt_document, "update_stock")
+				if not update_stock:
+					msg = _("Row {0}: Purchase Invoice {1} has no stock impact.").format(d.idx, frappe.bold(d.receipt_document))
+					msg += "<br>" + _("Please create Landed Cost Vouchers against Invoices that have 'Update Stock' enabled.")
+					frappe.throw(msg, title=_("Incorrect Invoice"))
+
+			receipt_documents.append(d.receipt_document)
 
 		for item in self.get("items"):
 			if not item.receipt_document:
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index e488b69..41800e3 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -254,6 +254,8 @@
 		return process_gl_map(gl_entries)
 
 	def make_item_gl_entries(self, gl_entries, warehouse_account=None):
+		from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import get_purchase_document_details
+
 		stock_rbnb = self.get_company_default("stock_received_but_not_billed")
 		landed_cost_entries = get_item_account_wise_additional_cost(self.name)
 		expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
@@ -262,6 +264,8 @@
 		warehouse_with_no_account = []
 		stock_items = self.get_stock_items()
 
+		exchange_rate_map, net_rate_map = get_purchase_document_details(self)
+
 		for d in self.get("items"):
 			if d.item_code in stock_items and flt(d.valuation_rate) and flt(d.qty):
 				if warehouse_account.get(d.warehouse):
@@ -304,6 +308,23 @@
 							-1 * flt(d.base_net_amount, d.precision("base_net_amount")), 0.0, remarks, warehouse_account_name,
 							debit_in_account_currency=-1 * credit_amount, account_currency=credit_currency, item=d)
 
+						# check if the exchange rate has changed
+						if d.get('purchase_invoice'):
+							if exchange_rate_map[d.purchase_invoice] and \
+								self.conversion_rate != exchange_rate_map[d.purchase_invoice] and \
+								d.net_rate == net_rate_map[d.purchase_invoice_item]:
+
+								discrepancy_caused_by_exchange_rate_difference = (d.qty * d.net_rate) * \
+									(exchange_rate_map[d.purchase_invoice] - self.conversion_rate)
+
+								self.add_gl_entry(gl_entries, account, d.cost_center, 0.0, discrepancy_caused_by_exchange_rate_difference,
+									remarks, self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference,
+									account_currency=credit_currency, item=d)
+
+								self.add_gl_entry(gl_entries, self.get_company_default("exchange_gain_loss_account"), d.cost_center, discrepancy_caused_by_exchange_rate_difference, 0.0,
+									remarks, self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference,
+									account_currency=credit_currency, item=d)
+
 					# Amount added through landed-cos-voucher
 					if d.landed_cost_voucher_amount and landed_cost_entries:
 						for account, amount in iteritems(landed_cost_entries[(d.item_code, d.name)]):
@@ -386,6 +407,7 @@
 			against_account = ", ".join([d.account for d in gl_entries if flt(d.debit) > 0])
 			total_valuation_amount = sum(valuation_tax.values())
 			amount_including_divisional_loss = negative_expense_to_be_booked
+			stock_rbnb = self.get_company_default("stock_received_but_not_billed")
 			i = 1
 			for tax in self.get("taxes"):
 				if valuation_tax.get(tax.name):
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index 2586a0f..dbba21f 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -1052,6 +1052,33 @@
 
 		frappe.db.set_value('Company', company, 'enable_perpetual_inventory_for_non_stock_items', before_test_value)
 
+	def test_purchase_receipt_with_exchange_rate_difference(self):
+		from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice as create_purchase_invoice
+		from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import make_purchase_receipt as create_purchase_receipt
+		
+		pi = create_purchase_invoice(company="_Test Company with perpetual inventory",
+			cost_center = "Main - TCP1",
+			warehouse = "Stores - TCP1",
+			expense_account ="_Test Account Cost for Goods Sold - TCP1",
+			currency = "USD", conversion_rate = 70)
+		
+		pr = create_purchase_receipt(pi.name)
+		pr.conversion_rate = 80
+		pr.items[0].purchase_invoice = pi.name
+		pr.items[0].purchase_invoice_item = pi.items[0].name
+
+		pr.save()
+		pr.submit()
+
+		# Get exchnage gain and loss account
+		exchange_gain_loss_account = frappe.db.get_value('Company', pr.company, 'exchange_gain_loss_account')
+
+		# fetching the latest GL Entry with exchange gain and loss account account
+		amount = frappe.db.get_value('GL Entry', {'account': exchange_gain_loss_account, 'voucher_no': pr.name}, 'credit')
+		discrepancy_caused_by_exchange_rate_diff = abs(pi.items[0].base_net_amount - pr.items[0].base_net_amount)
+
+		self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount)
+
 def get_sl_entries(voucher_type, voucher_no):
 	return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference
 		from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s
diff --git a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
index 7f3d701..f5d076a 100644
--- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
+++ b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
@@ -14,7 +14,7 @@
 )
 from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
 from erpnext.stock.doctype.item.test_item import create_item
-from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
+from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
 
 # test_records = frappe.get_test_records('Quality Inspection')
 
@@ -159,6 +159,47 @@
 			frappe.delete_doc("Quality Inspection", qi)
 		dn.delete()
 
+	def test_rejected_qi_validation(self):
+		"""Test if rejected QI blocks Stock Entry as per Stock Settings."""
+		se = make_stock_entry(
+			item_code="_Test Item with QA",
+			target="_Test Warehouse - _TC",
+			qty=1,
+			basic_rate=100,
+			inspection_required=True,
+			do_not_submit=True
+		)
+
+		readings = [
+			{
+				"specification": "Iron Content",
+				"min_value": 0.1,
+				"max_value": 0.9,
+				"reading_1": "0.4"
+			}
+		]
+
+		qa = create_quality_inspection(
+			reference_type="Stock Entry",
+			reference_name=se.name,
+			readings=readings,
+			status="Rejected"
+		)
+
+		frappe.db.set_value("Stock Settings", None, "action_if_quality_inspection_is_rejected", "Stop")
+		se.reload()
+		self.assertRaises(QualityInspectionRejectedError, se.submit) # when blocked in Stock settings, block rejected QI
+
+		frappe.db.set_value("Stock Settings", None, "action_if_quality_inspection_is_rejected", "Warn")
+		se.reload()
+		se.submit() # when allowed in Stock settings, allow rejected QI
+
+		# teardown
+		qa.reload()
+		qa.cancel()
+		se.reload()
+		se.cancel()
+		frappe.db.set_value("Stock Settings", None, "action_if_quality_inspection_is_rejected", "Stop")
 
 def create_quality_inspection(**args):
 	args = frappe._dict(args)
@@ -175,12 +216,11 @@
 	if not args.readings:
 		create_quality_inspection_parameter("Size")
 		readings = {"specification": "Size", "min_value": 0, "max_value": 10}
+		if args.status == "Rejected":
+			readings["reading_1"] = "12"  # status is auto set in child on save
 	else:
 		readings = args.readings
 
-	if args.status == "Rejected":
-		readings["reading_1"] = "12"  # status is auto set in child on save
-
 	if isinstance(readings, list):
 		for entry in readings:
 			create_quality_inspection_parameter(entry["specification"])
diff --git a/erpnext/stock/doctype/serial_no/serial_no.json b/erpnext/stock/doctype/serial_no/serial_no.json
index 3acf3a9..a3d44af 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.json
+++ b/erpnext/stock/doctype/serial_no/serial_no.json
@@ -57,7 +57,8 @@
   "more_info",
   "serial_no_details",
   "company",
-  "status"
+  "status",
+  "work_order"
  ],
  "fields": [
   {
@@ -422,12 +423,18 @@
    "label": "Status",
    "options": "\nActive\nInactive\nDelivered\nExpired",
    "read_only": 1
+  },
+  {
+   "fieldname": "work_order",
+   "fieldtype": "Link",
+   "label": "Work Order",
+   "options": "Work Order"
   }
  ],
  "icon": "fa fa-barcode",
  "idx": 1,
  "links": [],
- "modified": "2020-07-20 20:50:16.660433",
+ "modified": "2021-01-08 14:31:15.375996",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Serial No",
diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py
index b236f6a..bad7b60 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.py
+++ b/erpnext/stock/doctype/serial_no/serial_no.py
@@ -473,16 +473,13 @@
 		if s.strip()]
 
 def update_args_for_serial_no(serial_no_doc, serial_no, args, is_new=False):
-	serial_no_doc.update({
-		"item_code": args.get("item_code"),
-		"company": args.get("company"),
-		"batch_no": args.get("batch_no"),
-		"via_stock_ledger": args.get("via_stock_ledger") or True,
-		"supplier": args.get("supplier"),
-		"location": args.get("location"),
-		"warehouse": (args.get("warehouse")
-			if args.get("actual_qty", 0) > 0 else None)
-	})
+	for field in ["item_code", "work_order", "company", "batch_no", "supplier", "location"]:
+		if args.get(field):
+			serial_no_doc.set(field, args.get(field))
+
+	serial_no_doc.via_stock_ledger = args.get("via_stock_ledger") or True
+	serial_no_doc.warehouse = (args.get("warehouse")
+		if args.get("actual_qty", 0) > 0 else None)
 
 	if is_new:
 		serial_no_doc.serial_no = serial_no
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 66f8b63..8f27ef4 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -498,6 +498,7 @@
 				d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount"))
 				if not d.t_warehouse:
 					outgoing_items_cost += flt(d.basic_amount)
+
 		return outgoing_items_cost
 
 	def get_args_for_incoming_rate(self, item):
@@ -854,6 +855,7 @@
 				pro_doc.run_method("update_work_order_qty")
 				if self.purpose == "Manufacture":
 					pro_doc.run_method("update_planned_qty")
+					pro_doc.update_batch_produced_qty(self)
 
 			if not pro_doc.operations:
 				pro_doc.set_actual_dates()
@@ -1076,18 +1078,54 @@
 			# in case of BOM
 			to_warehouse = item.get("default_warehouse")
 
+		args = {
+			"to_warehouse": to_warehouse,
+			"from_warehouse": "",
+			"qty": self.fg_completed_qty,
+			"item_name": item.item_name,
+			"description": item.description,
+			"stock_uom": item.stock_uom,
+			"expense_account": item.get("expense_account"),
+			"cost_center": item.get("buying_cost_center"),
+			"is_finished_item": 1
+		}
+
+		if self.work_order and self.pro_doc.has_batch_no:
+			self.set_batchwise_finished_goods(args, item)
+		else:
+			self.add_finisged_goods(args, item)
+
+	def set_batchwise_finished_goods(self, args, item):
+		qty = flt(self.fg_completed_qty)
+		filters = {
+			"reference_name": self.pro_doc.name,
+			"reference_doctype": self.pro_doc.doctype,
+			"qty_to_produce": (">", 0)
+		}
+
+		fields = ["qty_to_produce as qty", "produced_qty", "name"]
+
+		for row in frappe.get_all("Batch", filters = filters, fields = fields, order_by="creation asc"):
+			batch_qty = flt(row.qty) - flt(row.produced_qty)
+			if not batch_qty:
+				continue
+
+			if qty <=0:
+				break
+
+			fg_qty = batch_qty
+			if batch_qty >= qty:
+				fg_qty = qty
+
+			qty -= batch_qty
+			args["qty"] = fg_qty
+			args["batch_no"] = row.name
+
+			self.add_finisged_goods(args, item)
+
+	def add_finisged_goods(self, args, item):
 		self.add_to_stock_entry_detail({
-			item.name: {
-				"to_warehouse": to_warehouse,
-				"from_warehouse": "",
-				"qty": self.fg_completed_qty,
-				"item_name": item.item_name,
-				"description": item.description,
-				"stock_uom": item.stock_uom,
-				"expense_account": item.get("expense_account"),
-				"cost_center": item.get("buying_cost_center"),
-				"is_finished_item": 1
-			}
+			item.name: args
 		}, bom_no = self.bom_no)
 
 	def get_bom_raw_materials(self, qty):
@@ -1524,6 +1562,36 @@
 				material_requests.append(material_request)
 				frappe.db.set_value('Material Request', material_request, 'transfer_status', status)
 
+	def set_serial_no_batch_for_finished_good(self):
+		args = {}
+		if self.pro_doc.serial_no:
+			self.get_serial_nos_for_fg(args)
+
+		for row in self.items:
+			if row.is_finished_item and row.item_code == self.pro_doc.production_item:
+				if args.get("serial_no"):
+					row.serial_no = '\n'.join(args["serial_no"][0: cint(row.qty)])
+
+	def get_serial_nos_for_fg(self, args):
+		fields = ["`tabStock Entry`.`name`", "`tabStock Entry Detail`.`qty`",
+			"`tabStock Entry Detail`.`serial_no`", "`tabStock Entry Detail`.`batch_no`"]
+
+		filters = [["Stock Entry","work_order","=",self.work_order], ["Stock Entry","purpose","=","Manufacture"],
+			["Stock Entry","docstatus","=",1], ["Stock Entry Detail","item_code","=",self.pro_doc.production_item]]
+
+		stock_entries = frappe.get_all("Stock Entry", fields=fields, filters=filters)
+
+		if self.pro_doc.serial_no:
+			args["serial_no"] = self.get_available_serial_nos(stock_entries)
+
+	def get_available_serial_nos(self, stock_entries):
+		used_serial_nos = []
+		for row in stock_entries:
+			if row.serial_no:
+				used_serial_nos.extend(get_serial_nos(row.serial_no))
+
+		return sorted(list(set(get_serial_nos(self.pro_doc.serial_no)) - set(used_serial_nos)))
+
 @frappe.whitelist()
 def move_sample_to_retention_warehouse(company, items):
 	if isinstance(items, string_types):
@@ -1635,6 +1703,10 @@
 		if bom.quantity:
 			operating_cost_per_unit = flt(bom.operating_cost) / flt(bom.quantity)
 
+	if work_order and work_order.produced_qty and cint(frappe.db.get_single_value('Manufacturing Settings',
+		'add_corrective_operation_cost_in_finished_good_valuation')):
+		operating_cost_per_unit += flt(work_order.corrective_operation_cost) / flt(work_order.produced_qty)
+
 	return operating_cost_per_unit
 
 def get_used_alternative_items(purchase_order=None, work_order=None):
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py
index b12a854..563fcb0 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py
@@ -45,6 +45,8 @@
 		s.posting_date = args.posting_date
 	if args.posting_time:
 		s.posting_time = args.posting_time
+	if args.inspection_required:
+		s.inspection_required = args.inspection_required
 
 	# map names
 	if args.from_warehouse:
diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
index 864ff48..22f412a 100644
--- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
+++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
@@ -18,6 +18,7 @@
   "col_break2",
   "is_finished_item",
   "is_scrap_item",
+  "quality_inspection",
   "subcontracted_item",
   "section_break_8",
   "description",
@@ -69,7 +70,6 @@
   "putaway_rule",
   "column_break_51",
   "reference_purchase_receipt",
-  "quality_inspection",
   "job_card_item"
  ],
  "fields": [
@@ -307,6 +307,7 @@
    "fieldname": "quality_inspection",
    "fieldtype": "Link",
    "label": "Quality Inspection",
+   "no_copy": 1,
    "options": "Quality Inspection"
   },
   {
@@ -548,7 +549,7 @@
  "index_web_pages_for_search": 1,
  "istable": 1,
  "links": [],
- "modified": "2021-02-11 13:47:50.158754",
+ "modified": "2021-06-21 16:03:18.834880",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Stock Entry Detail",
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 ba31ad7..af2ada8 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
@@ -54,7 +54,7 @@
 		)
 
 		# _Test Item for Reposting transferred from Stores to FG warehouse on 30-04-2020
-		make_stock_entry(
+		se = make_stock_entry(
 			item_code="_Test Item for Reposting",
 			source="Stores - _TC",
 			target="Finished Goods - _TC",
@@ -64,29 +64,29 @@
 			posting_date='2020-04-30',
 			posting_time='14:00'
 		)
-		target_wh_sle = get_previous_sle({
+		target_wh_sle = frappe.db.get_value('Stock Ledger Entry', {
 			"item_code": "_Test Item for Reposting",
 			"warehouse": "Finished Goods - _TC",
-			"posting_date": '2020-04-30',
-			"posting_time": '14:00'
-		})
+			"voucher_type": "Stock Entry",
+			"voucher_no": se.name
+		}, ["valuation_rate"], as_dict=1)
 
 		self.assertEqual(target_wh_sle.get("valuation_rate"), 150)
 
 		# Repack entry on 5-5-2020
 		repack = create_repack_entry(company=company, posting_date='2020-05-05', posting_time='14:00')
 
-		finished_item_sle = get_previous_sle({
+		finished_item_sle = frappe.db.get_value('Stock Ledger Entry', {
 			"item_code": "_Test Finished Item for Reposting",
 			"warehouse": "Finished Goods - _TC",
-			"posting_date": '2020-05-05',
-			"posting_time": '14:00'
-		})
+			"voucher_type": "Stock Entry",
+			"voucher_no": repack.name
+		}, ["incoming_rate", "valuation_rate"], as_dict=1)
 		self.assertEqual(finished_item_sle.get("incoming_rate"), 540)
 		self.assertEqual(finished_item_sle.get("valuation_rate"), 540)
 
 		# Reconciliation for _Test Item for Reposting at Stores on 12-04-2020: Qty = 50, Rate = 150
-		create_stock_reconciliation(
+		sr = create_stock_reconciliation(
 			item_code="_Test Item for Reposting",
 			warehouse="Stores - _TC",
 			qty=50,
@@ -109,12 +109,12 @@
 		self.assertEqual(target_wh_sle.get("valuation_rate"), 175)
 
 		# Check valuation rate of repacked item after back-dated entry at Stores
-		finished_item_sle = get_previous_sle({
+		finished_item_sle = frappe.db.get_value('Stock Ledger Entry', {
 			"item_code": "_Test Finished Item for Reposting",
 			"warehouse": "Finished Goods - _TC",
-			"posting_date": '2020-05-05',
-			"posting_time": '14:00'
-		})
+			"voucher_type": "Stock Entry",
+			"voucher_no": repack.name
+		}, ["incoming_rate", "valuation_rate"], as_dict=1)
 		self.assertEqual(finished_item_sle.get("incoming_rate"), 790)
 		self.assertEqual(finished_item_sle.get("valuation_rate"), 790)
 
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
index 3badc7e..76a3f1a 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
@@ -48,37 +48,54 @@
 	},
 
 	get_items: function(frm) {
-		frappe.prompt({label:"Warehouse", fieldname: "warehouse", fieldtype:"Link", options:"Warehouse", reqd: 1,
+		let fields = [{
+			label: 'Warehouse', fieldname: 'warehouse', fieldtype: 'Link', options: 'Warehouse', reqd: 1,
 			"get_query": function() {
 				return {
 					"filters": {
 						"company": frm.doc.company,
 					}
-				}
-			}},
-			function(data) {
-				frappe.call({
-					method:"erpnext.stock.doctype.stock_reconciliation.stock_reconciliation.get_items",
-					args: {
-						warehouse: data.warehouse,
-						posting_date: frm.doc.posting_date,
-						posting_time: frm.doc.posting_time,
-						company:frm.doc.company
-					},
-					callback: function(r) {
-						var items = [];
-						frm.clear_table("items");
-						for(var i=0; i< r.message.length; i++) {
-							var d = frm.add_child("items");
-							$.extend(d, r.message[i]);
-							if(!d.qty) d.qty = null;
-							if(!d.valuation_rate) d.valuation_rate = null;
-						}
-						frm.refresh_field("items");
-					}
-				});
+				};
 			}
-		, __("Get Items"), __("Update"));
+		}, {
+			label: "Item Code", fieldname: "item_code", fieldtype: "Link", options: "Item",
+			"get_query": function() {
+				return {
+					"filters": {
+						"disabled": 0,
+					}
+				};
+			}
+		}];
+
+		frappe.prompt(fields, function(data) {
+			frappe.call({
+				method: "erpnext.stock.doctype.stock_reconciliation.stock_reconciliation.get_items",
+				args: {
+					warehouse: data.warehouse,
+					posting_date: frm.doc.posting_date,
+					posting_time: frm.doc.posting_time,
+					company: frm.doc.company,
+					item_code: data.item_code
+				},
+				callback: function(r) {
+					frm.clear_table("items");
+					for (var i=0; i<r.message.length; i++) {
+						var d = frm.add_child("items");
+						$.extend(d, r.message[i]);
+
+						if (!d.qty) {
+							d.qty = 0;
+						}
+
+						if (!d.valuation_rate) {
+							d.valuation_rate = 0;
+						}
+					}
+					frm.refresh_field("items");
+				}
+			});
+		}, __("Get Items"), __("Update"));
 	},
 
 	posting_date: function(frm) {
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index 93ab40a..3e15d54 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -357,6 +357,7 @@
 			if row.current_qty:
 				data.actual_qty = -1 * row.current_qty
 				data.qty_after_transaction = flt(row.current_qty)
+				data.previous_qty_after_transaction = flt(row.qty)
 				data.valuation_rate = flt(row.current_valuation_rate)
 				data.stock_value = data.qty_after_transaction * data.valuation_rate
 				data.stock_value_difference = -1 * flt(row.amount_difference)
@@ -481,45 +482,99 @@
 			self._cancel()
 
 @frappe.whitelist()
-def get_items(warehouse, posting_date, posting_time, company):
+def get_items(warehouse, posting_date, posting_time, company, item_code=None):
+	items = [frappe._dict({
+		'item_code': item_code,
+		'warehouse': warehouse
+	})]
+
+	if not item_code:
+		items = get_items_for_stock_reco(warehouse, company)
+
+	res = []
+	itemwise_batch_data = get_itemwise_batch(warehouse, posting_date, company, item_code)
+
+	for d in items:
+		if d.item_code in itemwise_batch_data:
+			stock_bal = get_stock_balance(d.item_code, d.warehouse,
+				posting_date, posting_time, with_valuation_rate=True)
+
+			for row in itemwise_batch_data.get(d.item_code):
+				args = get_item_data(row, row.qty, stock_bal[1])
+				res.append(args)
+		else:
+			stock_bal = get_stock_balance(d.item_code, d.warehouse, posting_date, posting_time,
+				with_valuation_rate=True , with_serial_no=cint(d.has_serial_no))
+
+			args = get_item_data(d, stock_bal[0], stock_bal[1],
+				stock_bal[2] if cint(d.has_serial_no) else '')
+
+			res.append(args)
+
+	return res
+
+def get_items_for_stock_reco(warehouse, company):
 	lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"])
 	items = frappe.db.sql("""
-		select i.name, i.item_name, bin.warehouse, i.has_serial_no
+		select i.name as item_code, i.item_name, bin.warehouse as warehouse, i.has_serial_no, i.has_batch_no
 		from tabBin bin, tabItem i
-		where i.name=bin.item_code and i.disabled=0 and i.is_stock_item = 1
-		and i.has_variants = 0 and i.has_batch_no = 0
-		and exists(select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=bin.warehouse)
-	""", (lft, rgt))
+		where i.name=bin.item_code and IFNULL(i.disabled, 0) = 0 and i.is_stock_item = 1
+		and i.has_variants = 0 and exists(
+			select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=bin.warehouse
+		)
+	""", (lft, rgt), as_dict=1)
 
 	items += frappe.db.sql("""
-		select i.name, i.item_name, id.default_warehouse, i.has_serial_no
+		select i.name as item_code, i.item_name, id.default_warehouse as warehouse, i.has_serial_no, i.has_batch_no
 		from tabItem i, `tabItem Default` id
 		where i.name = id.parent
 			and exists(select name from `tabWarehouse` where lft >= %s and rgt <= %s and name=id.default_warehouse)
-			and i.is_stock_item = 1 and i.has_batch_no = 0
-			and i.has_variants = 0 and i.disabled = 0 and id.company=%s
+			and i.is_stock_item = 1 and i.has_variants = 0 and IFNULL(i.disabled, 0) = 0 and id.company=%s
 		group by i.name
-	""", (lft, rgt, company))
+	""", (lft, rgt, company), as_dict=1)
 
-	res = []
-	for d in set(items):
-		stock_bal = get_stock_balance(d[0], d[2], posting_date, posting_time,
-			with_valuation_rate=True , with_serial_no=cint(d[3]))
+	return items
 
-		if frappe.db.get_value("Item", d[0], "disabled") == 0:
-			res.append({
-				"item_code": d[0],
-				"warehouse": d[2],
-				"qty": stock_bal[0],
-				"item_name": d[1],
-				"valuation_rate": stock_bal[1],
-				"current_qty": stock_bal[0],
-				"current_valuation_rate": stock_bal[1],
-				"current_serial_no": stock_bal[2] if cint(d[3]) else '',
-				"serial_no": stock_bal[2] if cint(d[3]) else ''
-			})
+def get_item_data(row, qty, valuation_rate, serial_no=None):
+	return {
+		'item_code': row.item_code,
+		'warehouse': row.warehouse,
+		'qty': qty,
+		'item_name': row.item_name,
+		'valuation_rate': valuation_rate,
+		'current_qty': qty,
+		'current_valuation_rate': valuation_rate,
+		'current_serial_no': serial_no,
+		'serial_no': serial_no,
+		'batch_no': row.get('batch_no')
+	}
 
-	return res
+def get_itemwise_batch(warehouse, posting_date, company, item_code=None):
+	from erpnext.stock.report.batch_wise_balance_history.batch_wise_balance_history import execute
+	itemwise_batch_data = {}
+
+	filters = frappe._dict({
+		'warehouse': warehouse,
+		'from_date': posting_date,
+		'to_date': posting_date,
+		'company': company
+	})
+
+	if item_code:
+		filters.item_code = item_code
+
+	columns, data = execute(filters)
+
+	for row in data:
+		itemwise_batch_data.setdefault(row[0], []).append(frappe._dict({
+			'item_code': row[0],
+			'warehouse': warehouse,
+			'qty': row[8],
+			'item_name': row[1],
+			'batch_no': row[4]
+		}))
+
+	return itemwise_batch_data
 
 @frappe.whitelist()
 def get_stock_balance_for(item_code, warehouse,
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index 36380b8..7b98c7b 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -6,7 +6,7 @@
 
 from __future__ import unicode_literals
 import frappe, unittest
-from frappe.utils import flt, nowdate, nowtime
+from frappe.utils import flt, nowdate, nowtime, add_days
 from erpnext.accounts.utils import get_stock_and_account_balance
 from erpnext.stock.stock_ledger import get_previous_sle, update_entries_after
 from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import EmptyStockReconciliationItemsError, get_items
@@ -14,6 +14,7 @@
 from erpnext.stock.doctype.item.test_item import create_item
 from erpnext.stock.utils import get_incoming_rate, get_stock_value_on, get_valuation_method
 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
 
 class TestStockReconciliation(unittest.TestCase):
 	@classmethod
@@ -204,6 +205,117 @@
 		self.assertEqual(sr.get("items")[0].valuation_rate, 0)
 		self.assertEqual(sr.get("items")[0].amount, 0)
 
+	def test_backdated_stock_reco_qty_reposting(self):
+		"""
+			Test if a backdated stock reco recalculates future qty until next reco.
+			-------------------------------------------
+			Var		| Doc	|	Qty	| Balance
+			-------------------------------------------
+			SR5		| Reco	|	0	|	8	(posting date: today-4) [backdated]
+			PR1		| PR	|	10	|	18	(posting date: today-3)
+			PR2		| PR	|	1	|	19	(posting date: today-2)
+			SR4		| Reco	|	0	|	6	(posting date: today-1) [backdated]
+			PR3		| PR	|	1	|	7	(posting date: today) # can't post future PR
+		"""
+		item_code = "Backdated-Reco-Item"
+		warehouse = "_Test Warehouse - _TC"
+		create_item(item_code)
+
+		pr1 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=10, rate=100,
+			posting_date=add_days(nowdate(), -3))
+		pr2 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=1, rate=100,
+			posting_date=add_days(nowdate(), -2))
+		pr3 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=1, rate=100,
+			posting_date=nowdate())
+
+		pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0},
+			"qty_after_transaction")
+		pr3_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0},
+			"qty_after_transaction")
+		self.assertEqual(pr1_balance, 10)
+		self.assertEqual(pr3_balance, 12)
+
+		# post backdated stock reco in between
+		sr4 = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=6, rate=100,
+			posting_date=add_days(nowdate(), -1))
+		pr3_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0},
+			"qty_after_transaction")
+		self.assertEqual(pr3_balance, 7)
+
+		# post backdated stock reco at the start
+		sr5 = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=8, rate=100,
+			posting_date=add_days(nowdate(), -4))
+		pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0},
+			"qty_after_transaction")
+		pr2_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0},
+			"qty_after_transaction")
+		sr4_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": sr4.name, "is_cancelled": 0},
+			"qty_after_transaction")
+		self.assertEqual(pr1_balance, 18)
+		self.assertEqual(pr2_balance, 19)
+		self.assertEqual(sr4_balance, 6) # check if future stock reco is unaffected
+
+		# cancel backdated stock reco and check future impact
+		sr5.cancel()
+		pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0},
+			"qty_after_transaction")
+		pr2_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0},
+			"qty_after_transaction")
+		sr4_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": sr4.name, "is_cancelled": 0},
+			"qty_after_transaction")
+		self.assertEqual(pr1_balance, 10)
+		self.assertEqual(pr2_balance, 11)
+		self.assertEqual(sr4_balance, 6) # check if future stock reco is unaffected
+
+		# teardown
+		sr4.cancel()
+		pr3.cancel()
+		pr2.cancel()
+		pr1.cancel()
+
+	def test_backdated_stock_reco_future_negative_stock(self):
+		"""
+			Test if a backdated stock reco causes future negative stock and is blocked.
+			-------------------------------------------
+			Var		| Doc	|	Qty	| Balance
+			-------------------------------------------
+			PR1		| PR	|	10	|	10		(posting date: today-2)
+			SR3		| Reco	|	0	|	1		(posting date: today-1) [backdated & blocked]
+			DN2		| DN	|	-2	|	8(-1)	(posting date: today)
+		"""
+		from erpnext.stock.stock_ledger import NegativeStockError
+		from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
+
+		item_code = "Backdated-Reco-Item"
+		warehouse = "_Test Warehouse - _TC"
+		create_item(item_code)
+
+		negative_stock_setting = frappe.db.get_single_value("Stock Settings", "allow_negative_stock")
+		frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 0)
+
+		pr1 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=10, rate=100,
+			posting_date=add_days(nowdate(), -2))
+		dn2 = create_delivery_note(item_code=item_code, warehouse=warehouse, qty=2, rate=120,
+			posting_date=nowdate())
+
+		pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0},
+			"qty_after_transaction")
+		dn2_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": dn2.name, "is_cancelled": 0},
+			"qty_after_transaction")
+		self.assertEqual(pr1_balance, 10)
+		self.assertEqual(dn2_balance, 8)
+
+		# check if stock reco is blocked
+		sr3 = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=1, rate=100,
+			posting_date=add_days(nowdate(), -1), do_not_submit=True)
+		self.assertRaises(NegativeStockError, sr3.submit)
+
+		# teardown
+		frappe.db.set_value("Stock Settings", None, "allow_negative_stock", negative_stock_setting)
+		sr3.cancel()
+		dn2.cancel()
+		pr1.cancel()
+
 def insert_existing_sle(warehouse):
 	from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
 
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json
index cf5d98d..2a9dcfb 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.json
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.json
@@ -23,7 +23,10 @@
   "allow_negative_stock",
   "show_barcode_field",
   "clean_description_html",
+  "quality_inspection_settings_section",
   "action_if_quality_inspection_is_not_submitted",
+  "column_break_21",
+  "action_if_quality_inspection_is_rejected",
   "section_break_7",
   "automatically_set_serial_nos_based_on_fifo",
   "set_qty_in_transactions_based_on_serial_no_input",
@@ -264,6 +267,22 @@
   {
    "fieldname": "column_break_31",
    "fieldtype": "Column Break"
+  },
+  {
+   "fieldname": "quality_inspection_settings_section",
+   "fieldtype": "Section Break",
+   "label": "Quality Inspection Settings"
+  },
+  {
+   "fieldname": "column_break_21",
+   "fieldtype": "Column Break"
+  },
+  {
+   "default": "Stop",
+   "fieldname": "action_if_quality_inspection_is_rejected",
+   "fieldtype": "Select",
+   "label": "Action If Quality Inspection Is Rejected",
+   "options": "Stop\nWarn"
   }
  ],
  "icon": "icon-cog",
@@ -271,7 +290,7 @@
  "index_web_pages_for_search": 1,
  "issingle": 1,
  "links": [],
- "modified": "2021-04-30 17:27:42.709231",
+ "modified": "2021-07-10 16:17:42.159829",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Stock Settings",
diff --git a/erpnext/stock/report/incorrect_balance_qty_after_transaction/__init__.py b/erpnext/stock/report/incorrect_balance_qty_after_transaction/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/report/incorrect_balance_qty_after_transaction/__init__.py
diff --git a/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.js b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.js
new file mode 100644
index 0000000..bf11277
--- /dev/null
+++ b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.js
@@ -0,0 +1,27 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Incorrect Balance Qty After Transaction"] = {
+	"filters": [
+		{
+			label: __("Company"),
+			fieldtype: "Link",
+			fieldname: "company",
+			options: "Company",
+			default: frappe.defaults.get_user_default("Company"),
+			reqd: 1
+		},
+		{
+			label: __('Item Code'),
+			fieldtype: 'Link',
+			fieldname: 'item_code',
+			options: 'Item'
+		},
+		{
+			label: __('Warehouse'),
+			fieldtype: 'Link',
+			fieldname: 'warehouse'
+		}
+	]
+};
diff --git a/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.json b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.json
new file mode 100644
index 0000000..a5815bc
--- /dev/null
+++ b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.json
@@ -0,0 +1,32 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2021-05-12 16:47:58.717853",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2021-05-12 16:48:28.347575",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Incorrect Balance Qty After Transaction",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Stock Ledger Entry",
+ "report_name": "Incorrect Balance Qty After Transaction",
+ "report_type": "Script Report",
+ "roles": [
+  {
+   "role": "Stock User"
+  },
+  {
+   "role": "Stock Manager"
+  },
+  {
+   "role": "Purchase User"
+  }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py
new file mode 100644
index 0000000..cf174c9
--- /dev/null
+++ b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py
@@ -0,0 +1,111 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _
+from six import iteritems
+from frappe.utils import flt
+
+def execute(filters=None):
+	columns, data = [], []
+	columns = get_columns()
+	data = get_data(filters)
+	return columns, data
+
+def get_data(filters):
+	data = get_stock_ledger_entries(filters)
+	itewise_balance_qty = {}
+
+	for row in data:
+		key = (row.item_code, row.warehouse)
+		itewise_balance_qty.setdefault(key, []).append(row)
+
+	res = validate_data(itewise_balance_qty)
+	return res
+
+def validate_data(itewise_balance_qty):
+	res = []
+	for key, data in iteritems(itewise_balance_qty):
+		row = get_incorrect_data(data)
+		if row:
+			res.append(row)
+			res.append({})
+
+	return res
+
+def get_incorrect_data(data):
+	balance_qty = 0.0
+	for row in data:
+		balance_qty += row.actual_qty
+		if row.voucher_type == "Stock Reconciliation" and not row.batch_no:
+			balance_qty = flt(row.qty_after_transaction)
+
+		row.expected_balance_qty = balance_qty
+		if abs(flt(row.expected_balance_qty) - flt(row.qty_after_transaction)) > 0.5:
+			row.differnce = abs(flt(row.expected_balance_qty) - flt(row.qty_after_transaction))
+			return row
+
+def get_stock_ledger_entries(report_filters):
+	filters = {}
+	fields = ['name', 'voucher_type', 'voucher_no', 'item_code', 'actual_qty',
+		'posting_date', 'posting_time', 'company', 'warehouse', 'qty_after_transaction', 'batch_no']
+
+	for field in ['warehouse', 'item_code', 'company']:
+		if report_filters.get(field):
+			filters[field] = report_filters.get(field)
+
+	return frappe.get_all('Stock Ledger Entry', fields = fields, filters = filters,
+		order_by = 'timestamp(posting_date, posting_time) asc, creation asc')
+
+def get_columns():
+	return [{
+		'label': _('Id'),
+		'fieldtype': 'Link',
+		'fieldname': 'name',
+		'options': 'Stock Ledger Entry',
+		'width': 120
+	}, {
+		'label': _('Posting Date'),
+		'fieldtype': 'Date',
+		'fieldname': 'posting_date',
+		'width': 110
+	}, {
+		'label': _('Voucher Type'),
+		'fieldtype': 'Link',
+		'fieldname': 'voucher_type',
+		'options': 'DocType',
+		'width': 120
+	}, {
+		'label': _('Voucher No'),
+		'fieldtype': 'Dynamic Link',
+		'fieldname': 'voucher_no',
+		'options': 'voucher_type',
+		'width': 120
+	}, {
+		'label': _('Item Code'),
+		'fieldtype': 'Link',
+		'fieldname': 'item_code',
+		'options': 'Item',
+		'width': 120
+	}, {
+		'label': _('Warehouse'),
+		'fieldtype': 'Link',
+		'fieldname': 'warehouse',
+		'options': 'Warehouse',
+		'width': 120
+	}, {
+		'label': _('Expected Balance Qty'),
+		'fieldtype': 'Float',
+		'fieldname': 'expected_balance_qty',
+		'width': 170
+	}, {
+		'label': _('Actual Balance Qty'),
+		'fieldtype': 'Float',
+		'fieldname': 'qty_after_transaction',
+		'width': 150
+	}, {
+		'label': _('Difference'),
+		'fieldtype': 'Float',
+		'fieldname': 'differnce',
+		'width': 110
+	}]
\ No newline at end of file
diff --git a/erpnext/stock/report/incorrect_serial_no_valuation/__init__.py b/erpnext/stock/report/incorrect_serial_no_valuation/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/report/incorrect_serial_no_valuation/__init__.py
diff --git a/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.js b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.js
new file mode 100644
index 0000000..c62d480
--- /dev/null
+++ b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.js
@@ -0,0 +1,35 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Incorrect Serial No Valuation"] = {
+	"filters": [
+		{
+			label: __('Item Code'),
+			fieldtype: 'Link',
+			fieldname: 'item_code',
+			options: 'Item',
+			get_query: function() {
+				return {
+					filters: {
+						'has_serial_no': 1
+					}
+				}
+			}
+		},
+		{
+			label: __('From Date'),
+			fieldtype: 'Date',
+			fieldname: 'from_date',
+			reqd: 1,
+			default: frappe.defaults.get_user_default("year_start_date")
+		},
+		{
+			label: __('To Date'),
+			fieldtype: 'Date',
+			fieldname: 'to_date',
+			reqd: 1,
+			default: frappe.defaults.get_user_default("year_end_date")
+		}
+	]
+};
diff --git a/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.json b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.json
new file mode 100644
index 0000000..cc384a5
--- /dev/null
+++ b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.json
@@ -0,0 +1,36 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2021-05-13 13:07:00.767845",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "json": "{}",
+ "modified": "2021-05-13 13:07:00.767845",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Incorrect Serial No Valuation",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Stock Ledger Entry",
+ "report_name": "Incorrect Serial No Valuation",
+ "report_type": "Script Report",
+ "roles": [
+  {
+   "role": "Stock User"
+  },
+  {
+   "role": "Accounts Manager"
+  },
+  {
+   "role": "Accounts User"
+  },
+  {
+   "role": "Stock Manager"
+  }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py
new file mode 100644
index 0000000..e54cf4c
--- /dev/null
+++ b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py
@@ -0,0 +1,148 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+import copy
+from frappe import _
+from six import iteritems
+from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+
+def execute(filters=None):
+	columns, data = [], []
+	columns = get_columns()
+	data = get_data(filters)
+	return columns, data
+
+def get_data(filters):
+	data = get_stock_ledger_entries(filters)
+	serial_nos_data = prepare_serial_nos(data)
+	data = get_incorrect_serial_nos(serial_nos_data)
+
+	return data
+
+def prepare_serial_nos(data):
+	serial_no_wise_data = {}
+	for row in data:
+		if not row.serial_nos:
+			continue
+
+		for serial_no in get_serial_nos(row.serial_nos):
+			sle = copy.deepcopy(row)
+			sle.serial_no = serial_no
+			sle.qty = 1 if sle.actual_qty > 0 else -1
+			sle.valuation_rate = sle.valuation_rate if sle.actual_qty > 0 else sle.valuation_rate * -1
+			serial_no_wise_data.setdefault(serial_no, []).append(sle)
+
+	return serial_no_wise_data
+
+def get_incorrect_serial_nos(serial_nos_data):
+	result = []
+
+	total_value = frappe._dict({'qty': 0, 'valuation_rate': 0, 'serial_no': frappe.bold(_('Balance'))})
+
+	for serial_no, data in iteritems(serial_nos_data):
+		total_dict = frappe._dict({'qty': 0, 'valuation_rate': 0, 'serial_no': frappe.bold(_('Total'))})
+
+		if check_incorrect_serial_data(data, total_dict):
+			result.extend(data)
+
+			total_value.qty += total_dict.qty
+			total_value.valuation_rate += total_dict.valuation_rate
+
+			result.append(total_dict)
+			result.append({})
+
+	result.append(total_value)
+
+	return result
+
+def check_incorrect_serial_data(data, total_dict):
+	incorrect_data = False
+	for row in data:
+		total_dict.qty += row.qty
+		total_dict.valuation_rate += row.valuation_rate
+
+		if ((total_dict.qty == 0 and abs(total_dict.valuation_rate) > 0) or total_dict.qty < 0):
+			incorrect_data = True
+
+	return incorrect_data
+
+def get_stock_ledger_entries(report_filters):
+	fields = ['name', 'voucher_type', 'voucher_no', 'item_code', 'serial_no as serial_nos', 'actual_qty',
+		'posting_date', 'posting_time', 'company', 'warehouse', '(stock_value_difference / actual_qty) as valuation_rate']
+
+	filters = {'serial_no': ("is", "set")}
+
+	if report_filters.get('item_code'):
+		filters['item_code'] = report_filters.get('item_code')
+
+	if report_filters.get('from_date') and report_filters.get('to_date'):
+		filters['posting_date'] = ('between', [report_filters.get('from_date'), report_filters.get('to_date')])
+
+	return frappe.get_all('Stock Ledger Entry', fields = fields, filters = filters,
+		order_by = 'timestamp(posting_date, posting_time) asc, creation asc')
+
+def get_columns():
+	return [{
+		'label': _('Company'),
+		'fieldtype': 'Link',
+		'fieldname': 'company',
+		'options': 'Company',
+		'width': 120
+	}, {
+		'label': _('Id'),
+		'fieldtype': 'Link',
+		'fieldname': 'name',
+		'options': 'Stock Ledger Entry',
+		'width': 120
+	}, {
+		'label': _('Posting Date'),
+		'fieldtype': 'Date',
+		'fieldname': 'posting_date',
+		'width': 90
+	}, {
+		'label': _('Posting Time'),
+		'fieldtype': 'Time',
+		'fieldname': 'posting_time',
+		'width': 90
+	}, {
+		'label': _('Voucher Type'),
+		'fieldtype': 'Link',
+		'fieldname': 'voucher_type',
+		'options': 'DocType',
+		'width': 100
+	}, {
+		'label': _('Voucher No'),
+		'fieldtype': 'Dynamic Link',
+		'fieldname': 'voucher_no',
+		'options': 'voucher_type',
+		'width': 110
+	}, {
+		'label': _('Item Code'),
+		'fieldtype': 'Link',
+		'fieldname': 'item_code',
+		'options': 'Item',
+		'width': 120
+	}, {
+		'label': _('Warehouse'),
+		'fieldtype': 'Link',
+		'fieldname': 'warehouse',
+		'options': 'Warehouse',
+		'width': 120
+	}, {
+		'label': _('Serial No'),
+		'fieldtype': 'Link',
+		'fieldname': 'serial_no',
+		'options': 'Serial No',
+		'width': 100
+	}, {
+		'label': _('Qty'),
+		'fieldtype': 'Float',
+		'fieldname': 'qty',
+		'width': 80
+	}, {
+		'label': _('Valuation Rate (In / Out)'),
+		'fieldtype': 'Currency',
+		'fieldname': 'valuation_rate',
+		'width': 110
+	}]
\ No newline at end of file
diff --git a/erpnext/stock/report/incorrect_stock_value_report/__init__.py b/erpnext/stock/report/incorrect_stock_value_report/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/report/incorrect_stock_value_report/__init__.py
diff --git a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.js b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.js
new file mode 100644
index 0000000..ff42480
--- /dev/null
+++ b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.js
@@ -0,0 +1,36 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Incorrect Stock Value Report"] = {
+	"filters": [
+		{
+			"label": __("Company"),
+			"fieldname": "company",
+			"fieldtype": "Link",
+			"options": "Company",
+			"reqd": 1,
+			"default": frappe.defaults.get_user_default("Company")
+		},
+		{
+			"label": __("Account"),
+			"fieldname": "account",
+			"fieldtype": "Link",
+			"options": "Account",
+			get_query: function() {
+				var company = frappe.query_report.get_filter_value('company');
+				return {
+					filters: {
+						"account_type": "Stock",
+						"company": company
+					}
+				}
+			}
+		},
+		{
+			"label": __("From Date"),
+			"fieldname": "from_date",
+			"fieldtype": "Date"
+		}
+	]
+};
diff --git a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.json b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.json
new file mode 100644
index 0000000..a7e9f20
--- /dev/null
+++ b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.json
@@ -0,0 +1,29 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2021-06-22 15:35:05.148177",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2021-06-22 15:35:05.148177",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Incorrect Stock Value Report",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Stock Ledger Entry",
+ "report_name": "Incorrect Stock Value Report",
+ "report_type": "Script Report",
+ "roles": [
+  {
+   "role": "Stock User"
+  },
+  {
+   "role": "Accounts Manager"
+  }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py
new file mode 100644
index 0000000..a724387
--- /dev/null
+++ b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py
@@ -0,0 +1,141 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+import erpnext
+from frappe import _
+from six import iteritems
+from frappe.utils import add_days, today, getdate
+from erpnext.stock.utils import get_stock_value_on
+from erpnext.accounts.utils import get_stock_and_account_balance
+
+def execute(filters=None):
+	if not erpnext.is_perpetual_inventory_enabled(filters.company):
+		frappe.throw(_("Perpetual inventory required for the company {0} to view this report.")
+			.format(filters.company))
+
+	data = get_data(filters)
+	columns = get_columns(filters)
+
+	return columns, data
+
+def get_unsync_date(filters):
+	date = filters.from_date
+	if not date:
+		date = frappe.db.sql(""" SELECT min(posting_date) from `tabStock Ledger Entry`""")
+		date = date[0][0]
+
+	if not date:
+		return
+
+	while getdate(date) < getdate(today()):
+		account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(posting_date=date,
+			company=filters.company, account = filters.account)
+
+		if abs(account_bal - stock_bal) > 0.1:
+			return date
+
+		date = add_days(date, 1)
+
+def get_data(report_filters):
+	from_date = get_unsync_date(report_filters)
+
+	if not from_date:
+		return []
+
+	result = []
+
+	voucher_wise_dict = {}
+	data = frappe.db.sql('''
+			SELECT
+				name, posting_date, posting_time, voucher_type, voucher_no,
+				stock_value_difference, stock_value, warehouse, item_code
+			FROM
+				`tabStock Ledger Entry`
+			WHERE
+				posting_date
+				= %s and company = %s
+				and is_cancelled = 0
+			ORDER BY timestamp(posting_date, posting_time) asc, creation asc
+		''', (from_date, report_filters.company), as_dict=1)
+
+	for d in data:
+		voucher_wise_dict.setdefault((d.item_code, d.warehouse), []).append(d)
+
+	closing_date = add_days(from_date, -1)
+	for key, stock_data in iteritems(voucher_wise_dict):
+		prev_stock_value = get_stock_value_on(posting_date = closing_date, item_code=key[0], warehouse =key[1])
+		for data in stock_data:
+			expected_stock_value = prev_stock_value + data.stock_value_difference
+			if abs(data.stock_value - expected_stock_value) > 0.1:
+				data.difference_value = abs(data.stock_value - expected_stock_value)
+				data.expected_stock_value = expected_stock_value
+				result.append(data)
+
+	return result
+
+def get_columns(filters):
+	return [
+		{
+			"label": _("Stock Ledger ID"),
+			"fieldname": "name",
+			"fieldtype": "Link",
+			"options": "Stock Ledger Entry",
+			"width": "80"
+		},
+		{
+			"label": _("Posting Date"),
+			"fieldname": "posting_date",
+			"fieldtype": "Date"
+		},
+		{
+			"label": _("Posting Time"),
+			"fieldname": "posting_time",
+			"fieldtype": "Time"
+		},
+		{
+			"label": _("Voucher Type"),
+			"fieldname": "voucher_type",
+			"width": "110"
+		},
+		{
+			"label": _("Voucher No"),
+			"fieldname": "voucher_no",
+			"fieldtype": "Dynamic Link",
+			"options": "voucher_type",
+			"width": "110"
+		},
+		{
+			"label": _("Item Code"),
+			"fieldname": "item_code",
+			"fieldtype": "Link",
+			"options": "Item",
+			"width": "110"
+		},
+		{
+			"label": _("Warehouse"),
+			"fieldname": "warehouse",
+			"fieldtype": "Link",
+			"options": "Warehouse",
+			"width": "110"
+		},
+		{
+			"label": _("Expected Stock Value"),
+			"fieldname": "expected_stock_value",
+			"fieldtype": "Currency",
+			"width": "150"
+		},
+		{
+			"label": _("Stock Value"),
+			"fieldname": "stock_value",
+			"fieldtype": "Currency",
+			"width": "120"
+		},
+		{
+			"label": _("Difference Value"),
+			"fieldname": "difference_value",
+			"fieldtype": "Currency",
+			"width": "150"
+		}
+	]
\ No newline at end of file
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 9fe89c3..4e9c768 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -55,6 +55,11 @@
 				sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher)
 
 			args = sle_doc.as_dict()
+
+			if sle.get("voucher_type") == "Stock Reconciliation":
+				# preserve previous_qty_after_transaction for qty reposting
+				args.previous_qty_after_transaction = sle.get("previous_qty_after_transaction")
+
 			update_bin(args, allow_negative_stock, via_landed_cost_voucher)
 
 def get_args_for_future_sle(row):
@@ -215,7 +220,7 @@
 		"""
 		self.data.setdefault(args.warehouse, frappe._dict())
 		warehouse_dict = self.data[args.warehouse]
-		previous_sle = self.get_previous_sle_of_current_voucher(args)
+		previous_sle = get_previous_sle_of_current_voucher(args)
 		warehouse_dict.previous_sle = previous_sle
 
 		for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
@@ -227,29 +232,6 @@
 			"stock_value_difference": 0.0
 		})
 
-	def get_previous_sle_of_current_voucher(self, args):
-		"""get stock ledger entries filtered by specific posting datetime conditions"""
-
-		args['time_format'] = '%H:%i:%s'
-		if not args.get("posting_date"):
-			args["posting_date"] = "1900-01-01"
-		if not args.get("posting_time"):
-			args["posting_time"] = "00:00"
-
-		sle = frappe.db.sql("""
-			select *, timestamp(posting_date, posting_time) as "timestamp"
-			from `tabStock Ledger Entry`
-			where item_code = %(item_code)s
-				and warehouse = %(warehouse)s
-				and is_cancelled = 0
-				and timestamp(posting_date, time_format(posting_time, %(time_format)s)) < timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
-			order by timestamp(posting_date, posting_time) desc, creation desc
-			limit 1
-			for update""", args, as_dict=1)
-
-		return sle[0] if sle else frappe._dict()
-
-
 	def build(self):
 		from erpnext.controllers.stock_controller import future_sle_exists
 
@@ -734,6 +716,35 @@
 			bin_doc.flags.via_stock_ledger_entry = True
 			bin_doc.save(ignore_permissions=True)
 
+
+def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False):
+	"""get stock ledger entries filtered by specific posting datetime conditions"""
+
+	args['time_format'] = '%H:%i:%s'
+	if not args.get("posting_date"):
+		args["posting_date"] = "1900-01-01"
+	if not args.get("posting_time"):
+		args["posting_time"] = "00:00"
+
+	voucher_condition = ""
+	if exclude_current_voucher:
+		voucher_no = args.get("voucher_no")
+		voucher_condition = f"and voucher_no != '{voucher_no}'"
+
+	sle = frappe.db.sql("""
+		select *, timestamp(posting_date, posting_time) as "timestamp"
+		from `tabStock Ledger Entry`
+		where item_code = %(item_code)s
+			and warehouse = %(warehouse)s
+			and is_cancelled = 0
+			{voucher_condition}
+			and timestamp(posting_date, time_format(posting_time, %(time_format)s)) < timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
+		order by timestamp(posting_date, posting_time) desc, creation desc
+		limit 1
+		for update""".format(voucher_condition=voucher_condition), args, as_dict=1)
+
+	return sle[0] if sle else frappe._dict()
+
 def get_previous_sle(args, for_update=False):
 	"""
 		get the last sle on or before the current time-bucket,
@@ -862,9 +873,24 @@
 	return valuation_rate
 
 def update_qty_in_future_sle(args, allow_negative_stock=None):
+	"""Recalculate Qty after Transaction in future SLEs based on current SLE."""
+	datetime_limit_condition = ""
+	qty_shift = args.actual_qty
+
+	# find difference/shift in qty caused by stock reconciliation
+	if args.voucher_type == "Stock Reconciliation":
+		qty_shift = get_stock_reco_qty_shift(args)
+
+	# find the next nearest stock reco so that we only recalculate SLEs till that point
+	next_stock_reco_detail = get_next_stock_reco(args)
+	if next_stock_reco_detail:
+		detail = next_stock_reco_detail[0]
+		# add condition to update SLEs before this date & time
+		datetime_limit_condition = get_datetime_limit_condition(detail)
+
 	frappe.db.sql("""
 		update `tabStock Ledger Entry`
-		set qty_after_transaction = qty_after_transaction + {qty}
+		set qty_after_transaction = qty_after_transaction + {qty_shift}
 		where
 			item_code = %(item_code)s
 			and warehouse = %(warehouse)s
@@ -876,15 +902,70 @@
 					and creation > %(creation)s
 				)
 			)
-	""".format(qty=args.actual_qty), args)
+		{datetime_limit_condition}
+		""".format(qty_shift=qty_shift, datetime_limit_condition=datetime_limit_condition), args)
 
 	validate_negative_qty_in_future_sle(args, allow_negative_stock)
 
+def get_stock_reco_qty_shift(args):
+	stock_reco_qty_shift = 0
+	if args.get("is_cancelled"):
+		if args.get("previous_qty_after_transaction"):
+			# get qty (balance) that was set at submission
+			last_balance = args.get("previous_qty_after_transaction")
+			stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
+		else:
+			stock_reco_qty_shift = flt(args.actual_qty)
+	else:
+		# reco is being submitted
+		last_balance = get_previous_sle_of_current_voucher(args,
+			exclude_current_voucher=True).get("qty_after_transaction")
+
+		if last_balance is not None:
+			stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
+		else:
+			stock_reco_qty_shift = args.qty_after_transaction
+
+	return stock_reco_qty_shift
+
+def get_next_stock_reco(args):
+	"""Returns next nearest stock reconciliaton's details."""
+
+	return frappe.db.sql("""
+		select
+			name, posting_date, posting_time, creation, voucher_no
+		from
+			`tabStock Ledger Entry`
+		where
+			item_code = %(item_code)s
+			and warehouse = %(warehouse)s
+			and voucher_type = 'Stock Reconciliation'
+			and voucher_no != %(voucher_no)s
+			and is_cancelled = 0
+			and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)
+				or (
+					timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s)
+					and creation > %(creation)s
+				)
+			)
+		limit 1
+	""", args, as_dict=1)
+
+def get_datetime_limit_condition(detail):
+	return f"""
+		and
+		(timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}')
+			or (
+				timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}')
+				and creation < '{detail.creation}'
+			)
+		)"""
+
 def validate_negative_qty_in_future_sle(args, allow_negative_stock=None):
 	allow_negative_stock = allow_negative_stock \
 		or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
 
-	if args.actual_qty < 0 and not allow_negative_stock:
+	if (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation") and not allow_negative_stock:
 		sle = get_future_sle_with_negative_qty(args)
 		if sle:
 			message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
diff --git a/erpnext/stock/workspace/stock/stock.json b/erpnext/stock/workspace/stock/stock.json
index 3221dc4..529ce8e 100644
--- a/erpnext/stock/workspace/stock/stock.json
+++ b/erpnext/stock/workspace/stock/stock.json
@@ -15,6 +15,7 @@
  "hide_custom": 0,
  "icon": "stock",
  "idx": 0,
+ "is_default": 0,
  "is_standard": 1,
  "label": "Stock",
  "links": [
@@ -653,9 +654,44 @@
    "link_type": "Report",
    "onboard": 0,
    "type": "Link"
+  },
+  {
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Incorrect Data Report",
+   "link_type": "DocType",
+   "onboard": 0,
+   "type": "Card Break"
+  },
+  {
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Incorrect Serial No Qty and Valuation",
+   "link_to": "Incorrect Serial No Valuation",
+   "link_type": "Report",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Incorrect Balance Qty After Transaction",
+   "link_to": "Incorrect Balance Qty After Transaction",
+   "link_type": "Report",
+   "onboard": 0,
+   "type": "Link"
+  },
+  {
+   "hidden": 0,
+   "is_query_report": 0,
+   "label": "Stock and Account Value Comparison",
+   "link_to": "Stock and Account Value Comparison",
+   "link_type": "Report",
+   "onboard": 0,
+   "type": "Link"
   }
  ],
- "modified": "2020-12-01 13:38:36.282890",
+ "modified": "2021-05-13 13:10:24.914983",
  "modified_by": "Administrator",
  "module": "Stock",
  "name": "Stock",
diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json
index bc29821..14712f8 100644
--- a/erpnext/support/doctype/issue/issue.json
+++ b/erpnext/support/doctype/issue/issue.json
@@ -166,7 +166,7 @@
    "options": "Service Level Agreement"
   },
   {
-   "depends_on": "eval: doc.status != 'Replied';",
+   "depends_on": "eval: doc.status != 'Replied' && doc.service_level_agreement;",
    "fieldname": "response_by",
    "fieldtype": "Datetime",
    "label": "Response By",
@@ -180,7 +180,7 @@
    "read_only": 1
   },
   {
-   "depends_on": "eval: doc.status != 'Replied';",
+   "depends_on": "eval: doc.status != 'Replied' && doc.service_level_agreement;",
    "fieldname": "resolution_by",
    "fieldtype": "Datetime",
    "label": "Resolution By",
@@ -410,7 +410,7 @@
  "icon": "fa fa-ticket",
  "idx": 7,
  "links": [],
- "modified": "2021-05-26 10:49:07.574769",
+ "modified": "2021-06-10 03:22:27.098898",
  "modified_by": "Administrator",
  "module": "Support",
  "name": "Issue",
diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json
index 61ca3a3..de3389a 100644
--- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json
+++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json
@@ -150,7 +150,8 @@
    "fieldtype": "Link",
    "label": "Document Type",
    "options": "DocType",
-   "reqd": 1
+   "reqd": 1,
+   "set_only_once": 1
   },
   {
    "default": "1",
@@ -178,7 +179,7 @@
   }
  ],
  "links": [],
- "modified": "2021-05-29 13:35:41.956849",
+ "modified": "2021-07-08 12:28:46.283334",
  "modified_by": "Administrator",
  "module": "Support",
  "name": "Service Level Agreement",
diff --git a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py
index 2a8446d..7c18a65 100644
--- a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py
+++ b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py
@@ -81,10 +81,9 @@
 
 		# check SLA custom fields created for leads
 		sla_fields = get_service_level_agreement_fields()
-		meta = frappe.get_meta(doctype, cached=False)
 
 		for field in sla_fields:
-			self.assertTrue(meta.has_field(field.get("fieldname")))
+			self.assertTrue(frappe.db.exists("Custom Field", {"dt": doctype, "fieldname": field.get("fieldname")}))
 
 	def test_docfield_creation_for_sla_on_custom_dt(self):
 		doctype = create_custom_doctype()
@@ -102,10 +101,9 @@
 
 		# check SLA docfields created
 		sla_fields = get_service_level_agreement_fields()
-		meta = frappe.get_meta(doctype.name, cached=False)
 
 		for field in sla_fields:
-			self.assertTrue(meta.has_field(field.get("fieldname")))
+			self.assertTrue(frappe.db.exists("DocField", {"fieldname": field.get("fieldname"), "parent": doctype.name}))
 
 	def test_sla_application(self):
 		# Default Service Level Agreement
@@ -330,16 +328,11 @@
 			"entity": entity
 		})
 
-	service_level_agreement_exists = frappe.db.exists("Service Level Agreement", filters)
+	sla = frappe.db.exists("Service Level Agreement", filters)
+	if sla:
+		frappe.delete_doc("Service Level Agreement", sla, force=1)
 
-	if not service_level_agreement_exists:
-		doc = frappe.get_doc(service_level_agreement).insert(ignore_permissions=True)
-	else:
-		doc = frappe.get_doc("Service Level Agreement", service_level_agreement_exists)
-		doc.update(service_level_agreement)
-		doc.save()
-
-	return doc
+	return frappe.get_doc(service_level_agreement).insert(ignore_permissions=True)
 
 
 def create_customer():
diff --git a/erpnext/telephony/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py
index 4d553df..c00dfa9 100644
--- a/erpnext/telephony/doctype/call_log/call_log.py
+++ b/erpnext/telephony/doctype/call_log/call_log.py
@@ -142,7 +142,7 @@
 			for log in logs:
 				call_log = frappe.get_doc('Call Log', log)
 				call_log.add_link(link_type=doc.doctype, link_name=doc.name)
-				call_log.save()
+				call_log.save(ignore_permissions=True)
 			frappe.db.commit()
 	except Exception:
 		frappe.log_error(title=_('Error during caller information update'))
diff --git a/erpnext/templates/includes/projects/project_row.html b/erpnext/templates/includes/projects/project_row.html
index 4c8c40d..a256fbd 100644
--- a/erpnext/templates/includes/projects/project_row.html
+++ b/erpnext/templates/includes/projects/project_row.html
@@ -1,28 +1,54 @@
-{% if doc.status=="Open" %}
-<div class="web-list-item">
-	<a class="no-decoration" href="/projects?project={{ doc.name | urlencode }}">
-		<div class="row">
-			<div class="col-xs-6">
-
-				{{ doc.name }}
-			</div>
-			<div class="col-xs-3">
-				{% if doc.percent_complete %}
-					<div class="progress" style="margin-bottom: 0!important; margin-top: 10px!important; height:5px;">
-					  <div class="progress-bar progress-bar-{{ "warning" if doc.percent_complete|round < 100 else "success"}}" role="progressbar"
-					  	aria-valuenow="{{ doc.percent_complete|round|int }}"
-					  	aria-valuemin="0" aria-valuemax="100" style="width:{{ doc.percent_complete|round|int }}%;">
-					  </div>
-					</div>
-				{% else %}
-					<span class="indicator {{ "red" if doc.status=="Open" else "gray"  }}">
-						{{ doc.status }}</span>
-				{% endif %}
-			</div>
-			<div class="col-xs-3 text-right small text-muted">
-				{{ frappe.utils.pretty_date(doc.modified) }}
-			</div>
-		</div>
-	</a>
-</div>
+{% if doc.status == "Open" %}
+  <div class="web-list-item transaction-list-item">
+    <div class="row">
+      <div class="col-xs-2">
+        <a class="transaction-item-link" href="/projects?project={{ doc.name | urlencode }}">Link</a>
+        {{ doc.name }}
+      </div>
+      <div class="col-xs-2">
+        {{ doc.project_name }}
+      </div>
+      <div class="col-xs-3 text-center">
+        {% if doc.percent_complete %}
+          {% set pill_class = "green" if doc.percent_complete | round == 100 else
+            "orange" %}
+          <div class="ellipsis">
+            <span class="indicator-pill {{ pill_class }} filterable ellipsis">
+              <span>{{ frappe.utils.cint(doc.percent_complete) }}
+                %</span>
+            </span>
+          </div>
+        {% else %}
+          <span class="indicator-pill {{ " red" if doc.status=="Open" else "darkgrey" }}">
+            {{ doc.status }}</span>
+        {% endif %}
+      </div>
+      {% if doc["_assign"] %}
+        {% set assigned_users = json.loads(doc["_assign"])%}
+        <div class="col-xs-2">
+          {% for user in assigned_users %}
+            {% set user_details = frappe
+              .db
+              .get_value("User", user, [
+                "full_name", "user_image"
+              ], as_dict = True) %}
+            {% if user_details.user_image %}
+              <span class="avatar avatar-small" style="width:32px; height:32px;" title="{{ user_details.full_name }}">
+                <img src="{{ user_details.user_image }}">
+              </span>
+            {% else %}
+              <span class="avatar avatar-small" style="width:32px; height:32px;" title="{{ user_details.full_name }}">
+                <div class='standard-image' style="background-color: #F5F4F4; color: #000;">
+                  {{ frappe.utils.get_abbr(user_details.full_name) }}
+                </div>
+              </span>
+            {% endif %}
+          {% endfor %}
+        </div>
+      {% endif %}
+      <div class="col-xs-3 text-right small text-muted">
+        {{ frappe.utils.pretty_date(doc.modified) }}
+      </div>
+    </div>
+  </div>
 {% endif %}
diff --git a/erpnext/templates/includes/projects/project_tasks.html b/erpnext/templates/includes/projects/project_tasks.html
index 50b9f4b..2b07a5f 100644
--- a/erpnext/templates/includes/projects/project_tasks.html
+++ b/erpnext/templates/includes/projects/project_tasks.html
@@ -1,32 +1,5 @@
 {% for task in doc.tasks %}
-	<div class='task'>
-		<a class="no-decoration task-link {{ task.css_seen }}" href="/tasks?name={{ task.name }}">
-		<div class='row project-item'>
-			<div class='col-xs-9'>
-				<span class="indicator {{ "red" if task.status=="Open" else "green" if task.status=="Closed" else "gray" }}" title="{{ task.status }}"  > {{ task.subject }}</span>
-	 				<div class="small text-muted item-timestamp"
-	 					title="{{ frappe.utils.pretty_date(task.modified) }}">
-						{{ _("modified") }} {{ frappe.utils.pretty_date(task.modified) }}
-	 				</div>
-			</div>
-			<div class='col-xs-1'>{% if task.todo %}
-					{% if task.todo.user_image %}
-						<span class="avatar avatar-small" title="{{ task.todo.owner }}">
-							<img src="{{ task.todo.user_image }}">
-						</span>
-					{% else %}
-						<span class="avatar avatar-small standard-image" title="Assigned to {{ task.todo.owner }}">
-
-						</span>
-					{% endif %}
-				{% endif %}	 </div>
-			<div class='col-xs-2'>
-				<span class="pull-right list-comment-count small {{ "text-extra-muted" if task.comment_count==0 else "text-muted" }}">
-					<i class="octicon octicon-comment-discussion"></i>
-						{{ task.comment_count }}
-				</span>
-			</div>
-		</div>
-		</a>
-	</div>
+  <div class="web-list-item transaction-list-item">
+    {{ task_row(task, 0) }}
+  </div>
 {% endfor %}
diff --git a/erpnext/templates/includes/projects/project_timesheets.html b/erpnext/templates/includes/projects/project_timesheets.html
index 05a07c1..fa5b2f9 100644
--- a/erpnext/templates/includes/projects/project_timesheets.html
+++ b/erpnext/templates/includes/projects/project_timesheets.html
@@ -1,23 +1,33 @@
 {% for timesheet in doc.timesheets %}
-<div class='timesheet'>
-	<a class="no-decoration timesheet-link {{ timesheet.css_seen }}" href="/timesheet/{{ timesheet.info.name}}">
-		<div class='row project-item'>
-			<div class='col-xs-10'>
-				<span class="indicator {{ "blue" if timesheet.info.status=="Submitted" else "red" if timesheet.info.status=="Draft" else "gray" }}" title="{{ timesheet.info.status }}"  > {{ timesheet.info.name }} </span>
-				<div class="small text-muted item-timestamp">
-				{{ _("From") }} {{ frappe.format_date(timesheet.from_time) }} {{ _("to") }} {{ frappe.format_date(timesheet.to_time) }}
-			</div>
-			</div>
-				<div class='col-xs-1' style="margin-right:-30px;">
-				<span class="avatar avatar-small" title="{{ timesheet.info.modified_by }}"> <img src="{{ timesheet.info.user_image }}" style="display:flex;"></span>
-			</div>
-			<div class='col-xs-1'>
-				<span class="pull-right list-comment-count small {{ "text-extra-muted" if timesheet.comment_count==0 else "text-muted" }}">
-				<i class="octicon octicon-comment-discussion"></i>
-				{{ timesheet.info.comment_count }}
-				</span>
-			</div>
-		</div>
-	</a>
-</div>
-{% endfor %}
\ No newline at end of file
+  <div class="web-list-item transaction-list-item">
+    <div class="row">
+      <div class="col-xs-2">{{ timesheet.name }}</div>
+      <a class="transaction-item-link" href="/timesheet/{{ timesheet.name}}">Link</a>
+      <div class="col-xs-2">{{ timesheet.status }}</div>
+      <div class="col-xs-2">{{ frappe.utils.format_date(timesheet.from_time, "medium") }}</div>
+      <div class="col-xs-2">{{ frappe.utils.format_date(timesheet.to_time, "medium") }}</div>
+      <div class="col-xs-2">
+        {% set user_details = frappe
+          .db
+          .get_value("User", timesheet.modified_by, [
+            "full_name", "user_image"
+          ], as_dict = True)
+ %}
+        {% if user_details.user_image %}
+          <span class="avatar avatar-small" style="width:32px; height:32px;" title="{{ user_details.full_name }}">
+            <img src="{{ user_details.user_image }}">
+          </span>
+        {% else %}
+          <span class="avatar avatar-small" style="width:32px; height:32px;" title="{{ user_details.full_name }}">
+            <div class='standard-image' style='background-color: #F5F4F4; color: #000;'>
+              {{ frappe.utils.get_abbr(user_details.full_name) }}
+            </div>
+          </span>
+        {% endif %}
+      </div>
+      <div class="col-xs-2 text-right">
+        {{ frappe.utils.pretty_date(timesheet.modified) }}
+      </div>
+    </div>
+  </div>
+{% endfor %}
diff --git a/erpnext/templates/pages/projects.html b/erpnext/templates/pages/projects.html
index 7e294e0..76eaf75 100644
--- a/erpnext/templates/pages/projects.html
+++ b/erpnext/templates/pages/projects.html
@@ -1,90 +1,173 @@
 {% extends "templates/web.html" %}
 
-{% block title %}{{ doc.project_name }}{% endblock %}
+{% block title %}
+  {{ doc.project_name }}
+{% endblock %}
+
+{% block head_include %}
+  <link rel="stylesheet" href="/assets/frappe/css/font-awesome.css">
+{% endblock %}
 
 {% block header %}
-	<h1>{{ doc.project_name }}</h1>
+  <h1>{{ doc.project_name }}</h1>
 {% endblock %}
 
 {% block style %}
-	<style>
-		{% include "templates/includes/projects.css" %}
-	</style>
+  <style>
+    {
+      % include "templates/includes/projects.css"%
+    }
+  </style>
 {% endblock %}
 
-
 {% block page_content %}
-{% if doc.percent_complete %}
-<div class="progress progress-hg">
-	<div class="progress-bar progress-bar-{{ "warning" if doc.percent_complete|round < 100 else "success" }} active" 				role="progressbar" aria-valuenow="{{ doc.percent_complete|round|int }}"
-	aria-valuemin="0" aria-valuemax="100" style="width:{{ doc.percent_complete|round|int }}%;">
-	</div>
-</div>
-{% endif %}
 
-<div class="clearfix">
-  <h4 style="float: left;">{{ _("Tasks") }}</h4>
-  <a class="btn btn-secondary btn-light btn-sm" style="float: right; position: relative; top: 10px;" href='/tasks?new=1&project={{ doc.project_name }}'>{{ _("New task") }}</a>
-</div>
+  {{ progress_bar(doc.percent_complete) }}
 
-<p>
-<!-- <a class='small underline task-status-switch' data-status='Open'>{{ _("Show closed") }}</a> -->
-</p>
+  <div class="d-flex mt-5 mb-5 justify-content-between">
+    <h4>Status:</h4>
+    <h4>Progress:
+      <span>{{ doc.percent_complete }}
+        %</span>
+    </h4>
+    <h4>Hours Spent:
+      <span>{{ doc.actual_time }}</span>
+    </h4>
+  </div>
 
-{% if doc.tasks %}
-	<div class='project-task-section'>
-		<div class='project-task'>
-		{% include "erpnext/templates/includes/projects/project_tasks.html" %}
-		</div>
-		<p><a id= 'more-task' style='display: none;' class='more-tasks small underline'>{{ _("More") }}</a><p>
-	</div>
-{% else %}
-	<p class="text-muted">{{ _("No tasks") }}</p>
-{% endif %}
+  {{ progress_bar(doc.percent_complete) }}
 
+  {% if doc.tasks %}
+    <div class="website-list">
+      <div class="result">
+        <div class="web-list-item transaction-list-item">
+          <div class="row">
+            <h3 class="col-xs-4">Tasks</h3>
+            <h3 class="col-xs-2">Status</h3>
+            <h3 class="col-xs-2">End Date</h3>
+            <h3 class="col-xs-2">Assigned To</h3>
+            <div class="col-xs-2 text-right">
+              <a class="btn btn-secondary btn-light btn-sm" href='/tasks?new=1&project={{ doc.project_name }}'>{{ _("New task") }}</a>
+            </div>
+          </div>
+        </div>
+        {% include "erpnext/templates/includes/projects/project_tasks.html" %}
+      </div>
+    </div>
+  {% else %}
+    <p class="font-weight-bold">{{ _("No Tasks") }}</p>
+  {% endif %}
 
-<div class='padding'></div>
+  {% if doc.timesheets %}
+    <div class="website-list">
+      <div class="result">
+        <div class="web-list-item transaction-list-item">
+          <div class="row">
+            <h3 class="col-xs-2">Timesheets</h3>
+            <h3 class="col-xs-2">Status</h3>
+            <h3 class="col-xs-2">From</h3>
+            <h3 class="col-xs-2">To</h3>
+            <h3 class="col-xs-2">Modified By</h3>
+            <h3 class="col-xs-2 text-right">Modified On</h3>
+          </div>
+        </div>
+        {% include "erpnext/templates/includes/projects/project_timesheets.html" %}
+      </div>
+    </div>
+  {% else %}
+    <p class="font-weight-bold mt-5">{{ _("No Timesheets") }}</p>
+  {% endif %}
 
-<h4>{{ _("Timesheets") }}</h4>
+  {% if doc.attachments %}
+    <div class='padding'></div>
 
-{% if doc.timesheets %}
-	<div class='project-timelogs'>
-	{% include "erpnext/templates/includes/projects/project_timesheets.html" %}
-	</div>
-	{% if doc.timesheets|length > 9 %}
-		<p><a class='more-timelogs small underline'>{{ _("More") }}</a><p>
-	{% endif %}
-{% else %}
-	<p class="text-muted">{{ _("No time sheets") }}</p>
-{% endif %}
-
-{% if doc.attachments %}
-<div class='padding'></div>
-
-<h4>{{ _("Attachments") }}</h4>
-	<div class="project-attachments">
-		{% for attachment in doc.attachments %}
-		<div class="attachment">
-			<a class="no-decoration attachment-link" href="{{ attachment.file_url }}" target="blank">
-				<div class="row">
-					<div class="col-xs-9">
-						<span class="indicator red file-name"> {{ attachment.file_name }}</span>
-					</div>
-					<div class="col-xs-3">
-						<span class="pull-right file-size">{{ attachment.file_size }}</span>
-					</div>
-				</div>
-			</a>
-		</div>
-		{% endfor %}
-	</div>
-{% endif %}
+    <h4>{{ _("Attachments") }}</h4>
+    <div class="project-attachments">
+      {% for attachment in doc.attachments %}
+        <div class="attachment">
+          <a class="no-decoration attachment-link" href="{{ attachment.file_url }}" target="blank">
+            <div class="row">
+              <div class="col-xs-9">
+                <span class="indicator red file-name">
+                  {{ attachment.file_name }}</span>
+              </div>
+              <div class="col-xs-3">
+                <span class="pull-right file-size">{{ attachment.file_size }}</span>
+              </div>
+            </div>
+          </a>
+        </div>
+      {% endfor %}
+    </div>
+  {% endif %}
 
 </div>
 
 <script>
-	{% include "frappe/public/js/frappe/provide.js" %}
-	{% include "frappe/public/js/frappe/form/formatters.js" %}
+  { % include "frappe/public/js/frappe/provide.js" %
+  } { % include "frappe/public/js/frappe/form/formatters.js" %
+  }
 </script>
 
 {% endblock %}
+
+{% macro progress_bar(percent_complete) %}
+{% if percent_complete %}
+  <div class="progress progress-hg" style="height: 5px;">
+    <div class="progress-bar progress-bar-{{ 'warning' if percent_complete|round < 100 else 'success' }} active" role="progressbar" aria-valuenow="{{ percent_complete|round|int }}" aria-valuemin="0" aria-valuemax="100" style="width:{{ percent_complete|round|int }}%;"></div>
+  </div>
+{% else %}
+  <hr>
+{% endif %}
+{% endmacro %}
+
+{% macro task_row(task, indent) %}
+<div class="row mt-5 {% if task.children %} font-weight-bold {% endif %}">
+  <div class="col-xs-4">
+    <a class="nav-link " style="color: inherit; {% if task.parent_task %} margin-left: {{ indent }}px {% endif %}" href="/tasks?name={{ task.name | urlencode }}">
+      {% if task.parent_task %}
+        <span class="">
+          <i class="fa fa-level-up fa-rotate-90"></i>
+        </span>
+      {% endif %}
+      {{ task.subject }}</a>
+  </div>
+  <div class="col-xs-2">{{ task.status }}</div>
+  <div class="col-xs-2">
+    {% if task.exp_end_date %}
+      {{ task.exp_end_date }}
+    {% else %}
+      --
+    {% endif %}
+  </div>
+  <div class="col-xs-2">
+    {% if task["_assign"] %}
+      {% set assigned_users = json.loads(task["_assign"])%}
+      {% for user in assigned_users %}
+        {% set user_details = frappe.db.get_value("User", user,
+		["full_name", "user_image"],
+		as_dict = True)%}
+        {% if user_details.user_image %}
+          <span class="avatar avatar-small" style="width:32px; height:32px;" title="{{ user_details.full_name }}">
+            <img src="{{ user_details.user_image }}">
+          </span>
+        {% else %}
+          <span class="avatar avatar-small" style="width:32px; height:32px;" title="{{ user_details.full_name }}">
+            <div class='standard-image' style='background-color: #F5F4F4; color: #000;'>
+              {{ frappe.utils.get_abbr(user_details.full_name) }}
+            </div>
+          </span>
+        {% endif %}
+      {% endfor %}
+    {% endif %}
+  </div>
+  <div class="col-xs-2 text-right">
+    {{ frappe.utils.pretty_date(task.modified) }}
+  </div>
+</div>
+{% if task.children %}
+  {% for child in task.children %}
+    {{ task_row(child, indent + 30) }}
+  {% endfor %}
+{% endif %}
+{% endmacro %}
diff --git a/erpnext/templates/pages/projects.py b/erpnext/templates/pages/projects.py
index d23fed9..7ff4954 100644
--- a/erpnext/templates/pages/projects.py
+++ b/erpnext/templates/pages/projects.py
@@ -32,29 +32,17 @@
 	filters = {"project": project}
 	if search:
 		filters["subject"] = ("like", "%{0}%".format(search))
-	# if item_status:
-# 		filters["status"] = item_status
 	tasks = frappe.get_all("Task", filters=filters,
-		fields=["name", "subject", "status", "_seen", "_comments", "modified", "description"],
+		fields=["name", "subject", "status", "modified", "_assign", "exp_end_date", "is_group", "parent_task"],
 		limit_start=start, limit_page_length=10)
-
+	task_nest = []
 	for task in tasks:
-		task.todo = frappe.get_all('ToDo',filters={'reference_name':task.name, 'reference_type':'Task'},
-		fields=["assigned_by", "owner", "modified", "modified_by"])
-
-		if task.todo:
-			task.todo=task.todo[0]
-			task.todo.user_image = frappe.db.get_value('User', task.todo.owner, 'user_image')
-
-
-		task.comment_count = len(json.loads(task._comments or "[]"))
-
-		task.css_seen = ''
-		if task._seen:
-			if frappe.session.user in json.loads(task._seen):
-				task.css_seen = 'seen'
-
-	return tasks
+		if task.is_group:
+			child_tasks = list(filter(lambda x: x.parent_task == task.name, tasks))
+			if len(child_tasks):
+				task.children = child_tasks
+		task_nest.append(task)
+	return list(filter(lambda x: not x.parent_task, tasks))
 
 @frappe.whitelist()
 def get_task_html(project, start=0, item_status=None):
@@ -74,19 +62,11 @@
 	fields=['project','activity_type','from_time','to_time','parent'],
 	limit_start=start, limit_page_length=10)
 	for timesheet in timesheets:
-		timesheet.infos = frappe.get_all('Timesheet', filters={"name": timesheet.parent},
-			fields=['name','_comments','_seen','status','modified','modified_by'],
+		info = frappe.get_all('Timesheet', filters={"name": timesheet.parent},
+			fields=['name','status','modified','modified_by'],
 			limit_start=start, limit_page_length=10)
-
-		for timesheet.info in timesheet.infos:
-			timesheet.info.user_image = frappe.db.get_value('User', timesheet.info.modified_by, 'user_image')
-
-			timesheet.info.comment_count = len(json.loads(timesheet.info._comments or "[]"))
-
-			timesheet.info.css_seen = ''
-			if timesheet.info._seen:
-				if frappe.session.user in json.loads(timesheet.info._seen):
-					timesheet.info.css_seen = 'seen'
+		if len(info):
+			timesheet.update(info[0])
 	return timesheets
 
 @frappe.whitelist()
diff --git a/erpnext/www/book_appointment/index.py b/erpnext/www/book_appointment/index.py
index 7bfac89..4f45561 100644
--- a/erpnext/www/book_appointment/index.py
+++ b/erpnext/www/book_appointment/index.py
@@ -2,7 +2,7 @@
 import datetime
 import json
 import pytz
-
+from frappe import _
 
 WEEKDAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
 
@@ -14,7 +14,8 @@
 	if is_enabled:
 		return context
 	else:
-		frappe.local.flags.redirect_location = '/404'
+		frappe.redirect_to_message(_("Appointment Scheduling Disabled"), _("Appointment Scheduling has been disabled for this site"),
+			http_status_code=302, indicator_color="red")
 		raise frappe.Redirect
 
 @frappe.whitelist(allow_guest=True)
@@ -146,4 +147,4 @@
 
 def _datetime_to_deltatime(date_time):
 	midnight = datetime.datetime.combine(date_time.date(), datetime.time.min)
-	return (date_time-midnight)
\ No newline at end of file
+	return (date_time-midnight)