Merge branch 'develop' into subcontracting
diff --git a/.github/helper/.flake8_strict b/.github/helper/.flake8_strict
index a79137d..198ec7b 100644
--- a/.github/helper/.flake8_strict
+++ b/.github/helper/.flake8_strict
@@ -66,6 +66,7 @@
     F841,
     E713,
     E712,
+    B023
 
 
 max-line-length = 200
diff --git a/.github/workflows/docs-checker.yml b/.github/workflows/docs-checker.yml
index b644568..722c125 100644
--- a/.github/workflows/docs-checker.yml
+++ b/.github/workflows/docs-checker.yml
@@ -12,7 +12,7 @@
       - name: 'Setup Environment'
         uses: actions/setup-python@v2
         with:
-          python-version: 3.8
+          python-version: '3.10'
 
       - name: 'Clone repo'
         uses: actions/checkout@v2
diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml
index 2cf4444..a71db72 100644
--- a/.github/workflows/patch.yml
+++ b/.github/workflows/patch.yml
@@ -35,9 +35,9 @@
         uses: actions/checkout@v2
 
       - name: Setup Python
-        uses: actions/setup-python@v2
+        uses: "gabrielfalcao/pyenv-action@v9"
         with:
-          python-version: 3.8
+          versions: 3.10:latest, 3.7:latest
 
       - name: Setup Node
         uses: actions/setup-node@v2
@@ -52,7 +52,7 @@
         uses: actions/cache@v2
         with:
           path: ~/.cache/pip
-          key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
+          key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }}
           restore-keys: |
             ${{ runner.os }}-pip-
             ${{ runner.os }}-
@@ -82,7 +82,10 @@
             ${{ runner.os }}-yarn-
 
       - name: Install
-        run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
+        run: |
+          pip install frappe-bench
+          pyenv global $(pyenv versions | grep '3.10')
+          bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
         env:
           DB: mariadb
           TYPE: server
@@ -96,18 +99,23 @@
           git -C "apps/frappe" remote set-url upstream https://github.com/frappe/frappe.git
           git -C "apps/erpnext" remote set-url upstream https://github.com/frappe/erpnext.git
 
+          pyenv global $(pyenv versions | grep '3.7')
           for version in $(seq 12 13)
           do
               echo "Updating to v$version"
               branch_name="version-$version-hotfix"
 
+
               git -C "apps/frappe" fetch --depth 1 upstream $branch_name:$branch_name
               git -C "apps/erpnext" fetch --depth 1 upstream $branch_name:$branch_name
 
               git -C "apps/frappe" checkout -q -f $branch_name
               git -C "apps/erpnext" checkout -q -f $branch_name
 
-              bench setup requirements --python
+              rm -rf ~/frappe-bench/env
+              bench setup env
+              bench pip install -e ./apps/erpnext
+
               bench --site test_site migrate
           done
 
@@ -115,5 +123,10 @@
           echo "Updating to latest version"
           git -C "apps/frappe" checkout -q -f "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}"
           git -C "apps/erpnext" checkout -q -f "$GITHUB_SHA"
-          bench setup requirements --python
+
+          pyenv global $(pyenv versions | grep '3.10')
+          rm -rf ~/frappe-bench/env
+          bench -v setup env
+          bench pip install -e ./apps/erpnext
+
           bench --site test_site migrate
diff --git a/.github/workflows/server-tests-mariadb.yml b/.github/workflows/server-tests-mariadb.yml
index 91a0114..f65cb46 100644
--- a/.github/workflows/server-tests-mariadb.yml
+++ b/.github/workflows/server-tests-mariadb.yml
@@ -59,7 +59,7 @@
       - name: Setup Python
         uses: actions/setup-python@v2
         with:
-          python-version: 3.8
+          python-version: '3.10'
 
       - name: Setup Node
         uses: actions/setup-node@v2
@@ -74,7 +74,7 @@
         uses: actions/cache@v2
         with:
           path: ~/.cache/pip
-          key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
+          key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }}
           restore-keys: |
             ${{ runner.os }}-pip-
             ${{ runner.os }}-
diff --git a/.github/workflows/server-tests-postgres.yml b/.github/workflows/server-tests-postgres.yml
index d3268e2..53a94db 100644
--- a/.github/workflows/server-tests-postgres.yml
+++ b/.github/workflows/server-tests-postgres.yml
@@ -46,7 +46,7 @@
       - name: Setup Python
         uses: actions/setup-python@v2
         with:
-          python-version: 3.8
+          python-version: '3.10'
 
       - name: Setup Node
         uses: actions/setup-node@v2
@@ -61,7 +61,7 @@
         uses: actions/cache@v2
         with:
           path: ~/.cache/pip
-          key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
+          key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }}
           restore-keys: |
             ${{ runner.os }}-pip-
             ${{ runner.os }}-
diff --git a/CODEOWNERS b/CODEOWNERS
index bfc2601..ecbae86 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -3,33 +3,30 @@
 # These owners will be the default owners for everything in
 # the repo. Unless a later match takes precedence,
 
-erpnext/accounts/               @nextchamp-saqib @deepeshgarg007
-erpnext/assets/                 @nextchamp-saqib @deepeshgarg007
-erpnext/erpnext_integrations/   @nextchamp-saqib
+erpnext/accounts/               @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
+erpnext/assets/                 @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
 erpnext/loan_management/        @nextchamp-saqib @deepeshgarg007
-erpnext/regional                @nextchamp-saqib @deepeshgarg007
-erpnext/selling                 @nextchamp-saqib @deepeshgarg007
+erpnext/regional                @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
+erpnext/selling                 @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
 erpnext/support/                @nextchamp-saqib @deepeshgarg007
 pos*                            @nextchamp-saqib
 
-erpnext/buying/                 @marination @rohitwaghchaure @ankush
+erpnext/buying/                 @marination @rohitwaghchaure @s-aga-r
 erpnext/e_commerce/             @marination
-erpnext/maintenance/            @marination @rohitwaghchaure
-erpnext/manufacturing/          @marination @rohitwaghchaure @ankush
+erpnext/maintenance/            @marination @rohitwaghchaure @s-aga-r
+erpnext/manufacturing/          @marination @rohitwaghchaure @s-aga-r
 erpnext/portal/                 @marination
-erpnext/quality_management/     @marination @rohitwaghchaure
+erpnext/quality_management/     @marination @rohitwaghchaure @s-aga-r
 erpnext/shopping_cart/          @marination
-erpnext/stock/                  @marination @rohitwaghchaure @ankush
+erpnext/stock/                  @marination @rohitwaghchaure @s-aga-r
 
-erpnext/crm/                    @ruchamahabal @pateljannat
-erpnext/education/              @ruchamahabal @pateljannat
-erpnext/hr/                     @ruchamahabal @pateljannat
-erpnext/payroll                 @ruchamahabal @pateljannat
-erpnext/projects/               @ruchamahabal @pateljannat
+erpnext/crm/                    @NagariaHussain 
+erpnext/education/              @rutwikhdev 
+erpnext/projects/               @ruchamahabal
 
-erpnext/controllers/            @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination @ankush
-erpnext/patches/                @deepeshgarg007 @nextchamp-saqib @marination @ankush
+erpnext/controllers/            @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination
+erpnext/patches/                @deepeshgarg007 @nextchamp-saqib @marination
 erpnext/public/                 @nextchamp-saqib @marination
 
 .github/                        @ankush
-requirements.txt                @gavindsouza
+pyproject.toml                  @gavindsouza @ankush
diff --git a/dev-requirements.txt b/dev-requirements.txt
deleted file mode 100644
index 15545c0..0000000
--- a/dev-requirements.txt
+++ /dev/null
@@ -1 +0,0 @@
-hypothesis~=6.31.0
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
index 5b2b526..5ed34d3 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
@@ -162,7 +162,7 @@
 							{
 								"reference_type": inv.voucher_type,
 								"reference_name": inv.voucher_no,
-								"amount": -(inv.outstanding),
+								"amount": -(inv.outstanding_in_account_currency),
 								"posting_date": inv.posting_date,
 								"currency": inv.currency,
 							}
diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
index 575ac74..325346d 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
@@ -19,6 +19,7 @@
 		self.create_company()
 		self.create_item()
 		self.create_customer()
+		self.create_account()
 		self.clear_old_entries()
 
 	def tearDown(self):
@@ -89,6 +90,38 @@
 			customer.save()
 			self.customer2 = customer.name
 
+		if frappe.db.exists("Customer", "_Test PR Customer 3"):
+			self.customer3 = "_Test PR Customer 3"
+		else:
+			customer = frappe.new_doc("Customer")
+			customer.customer_name = "_Test PR Customer 3"
+			customer.type = "Individual"
+			customer.default_currency = "EUR"
+			customer.save()
+			self.customer3 = customer.name
+
+	def create_account(self):
+		account_name = "Debtors EUR"
+		if not frappe.db.get_value(
+			"Account", filters={"account_name": account_name, "company": self.company}
+		):
+			acc = frappe.new_doc("Account")
+			acc.account_name = account_name
+			acc.parent_account = "Accounts Receivable - _PR"
+			acc.company = self.company
+			acc.account_currency = "EUR"
+			acc.account_type = "Receivable"
+			acc.insert()
+		else:
+			name = frappe.db.get_value(
+				"Account",
+				filters={"account_name": account_name, "company": self.company},
+				fieldname="name",
+				pluck=True,
+			)
+			acc = frappe.get_doc("Account", name)
+		self.debtors_eur = acc.name
+
 	def create_sales_invoice(
 		self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
 	):
@@ -454,3 +487,56 @@
 		self.assertEqual(len(pr.get("payments")), 1)
 		self.assertEqual(pr.get("invoices")[0].outstanding_amount, 20)
 		self.assertEqual(pr.get("payments")[0].amount, 20)
+
+	def test_pr_output_foreign_currency_and_amount(self):
+		# test for currency and amount invoices and payments
+		transaction_date = nowdate()
+		# In EUR
+		amount = 100
+		exchange_rate = 80
+
+		si = self.create_sales_invoice(
+			qty=1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True
+		)
+		si.customer = self.customer3
+		si.currency = "EUR"
+		si.conversion_rate = exchange_rate
+		si.debit_to = self.debtors_eur
+		si = si.save().submit()
+
+		cr_note = self.create_sales_invoice(
+			qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True
+		)
+		cr_note.customer = self.customer3
+		cr_note.is_return = 1
+		cr_note.currency = "EUR"
+		cr_note.conversion_rate = exchange_rate
+		cr_note.debit_to = self.debtors_eur
+		cr_note = cr_note.save().submit()
+
+		pr = self.create_payment_reconciliation()
+		pr.party = self.customer3
+		pr.receivable_payable_account = self.debtors_eur
+		pr.get_unreconciled_entries()
+
+		self.assertEqual(len(pr.invoices), 1)
+		self.assertEqual(len(pr.payments), 1)
+
+		self.assertEqual(pr.invoices[0].amount, amount)
+		self.assertEqual(pr.invoices[0].currency, "EUR")
+		self.assertEqual(pr.payments[0].amount, amount)
+		self.assertEqual(pr.payments[0].currency, "EUR")
+
+		cr_note.cancel()
+
+		from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
+
+		pay = get_payment_entry(si.doctype, si.name)
+		pay.references.clear()
+		pay = pay.save().submit()
+
+		pr.get_unreconciled_entries()
+		self.assertEqual(len(pr.invoices), 1)
+		self.assertEqual(len(pr.payments), 1)
+		self.assertEqual(pr.payments[0].amount, amount)
+		self.assertEqual(pr.payments[0].currency, "EUR")
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index aefa9a5..cdb187f 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -476,6 +476,13 @@
 			this.frm.trigger("calculate_timesheet_totals");
 		}
 	}
+
+	is_cash_or_non_trade_discount() {
+		this.frm.set_df_property("additional_discount_account", "hidden", 1 - this.frm.doc.is_cash_or_non_trade_discount);
+		if (!this.frm.doc.is_cash_or_non_trade_discount) {
+			this.frm.set_value("additional_discount_account", "");
+		}
+	}
 };
 
 // for backward compatibility: combine new and previous states
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index 327545a..499377d 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -106,6 +106,7 @@
   "loyalty_redemption_cost_center",
   "section_break_49",
   "apply_discount_on",
+  "is_cash_or_non_trade_discount",
   "base_discount_amount",
   "additional_discount_account",
   "column_break_51",
@@ -1790,8 +1791,6 @@
    "width": "50%"
   },
   {
-   "fetch_from": "sales_partner.commission_rate",
-   "fetch_if_empty": 1,
    "fieldname": "commission_rate",
    "fieldtype": "Float",
    "hide_days": 1,
@@ -1990,7 +1989,7 @@
   {
    "fieldname": "additional_discount_account",
    "fieldtype": "Link",
-   "label": "Additional Discount Account",
+   "label": "Discount Account",
    "options": "Account"
   },
   {
@@ -2028,6 +2027,13 @@
    "fieldtype": "Currency",
    "label": "Amount Eligible for Commission",
    "read_only": 1
+  },
+  {
+   "default": "0",
+   "depends_on": "eval: doc.apply_discount_on == \"Grand Total\"",
+   "fieldname": "is_cash_or_non_trade_discount",
+   "fieldtype": "Check",
+   "label": "Is Cash or Non Trade Discount"
   }
  ],
  "icon": "fa fa-file-text",
@@ -2040,7 +2046,7 @@
    "link_fieldname": "consolidated_invoice"
   }
  ],
- "modified": "2022-06-10 03:52:51.409913",
+ "modified": "2022-06-16 16:22:44.870575",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Sales Invoice",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 1a3164b..6530e5c 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -1030,7 +1030,7 @@
 		)
 
 		if grand_total and not self.is_internal_transfer():
-			# Didnot use base_grand_total to book rounding loss gle
+			# Did not use base_grand_total to book rounding loss gle
 			gl_entries.append(
 				self.get_gl_dict(
 					{
@@ -1055,6 +1055,22 @@
 				)
 			)
 
+		if self.apply_discount_on == "Grand Total" and self.get("is_cash_or_discount_account"):
+			gl_entries.append(
+				self.get_gl_dict(
+					{
+						"account": self.additional_discount_account,
+						"against": self.debit_to,
+						"debit": self.base_discount_amount,
+						"debit_in_account_currency": self.discount_amount,
+						"cost_center": self.cost_center,
+						"project": self.project,
+					},
+					self.currency,
+					item=self,
+				)
+			)
+
 	def make_tax_gl_entries(self, gl_entries):
 		enable_discount_accounting = cint(
 			frappe.db.get_single_value("Selling Settings", "enable_discount_accounting")
@@ -2117,6 +2133,8 @@
 		source_document_warehouse_field = "from_warehouse"
 		target_document_warehouse_field = "target_warehouse"
 
+	received_items = get_received_items(source_name, target_doctype, target_detail_field)
+
 	validate_inter_company_transaction(source_doc, doctype)
 	details = get_inter_company_details(source_doc, doctype)
 
@@ -2181,12 +2199,17 @@
 				shipping_address_name=target_doc.shipping_address_name,
 			)
 
+	def update_item(source, target, source_parent):
+		target.qty = flt(source.qty) - received_items.get(source.name, 0.0)
+
 	item_field_map = {
 		"doctype": target_doctype + " Item",
 		"field_no_map": ["income_account", "expense_account", "cost_center", "warehouse"],
 		"field_map": {
 			"rate": "rate",
 		},
+		"postprocess": update_item,
+		"condition": lambda doc: doc.qty > 0,
 	}
 
 	if doctype in ["Sales Invoice", "Sales Order"]:
@@ -2224,6 +2247,28 @@
 	return doclist
 
 
+def get_received_items(reference_name, doctype, reference_fieldname):
+	target_doctypes = frappe.get_all(
+		doctype,
+		filters={"inter_company_invoice_reference": reference_name, "docstatus": 1},
+		as_list=True,
+	)
+
+	if target_doctypes:
+		target_doctypes = list(target_doctypes[0])
+
+	received_items_map = frappe._dict(
+		frappe.get_all(
+			doctype + " Item",
+			filters={"parent": ("in", target_doctypes)},
+			fields=[reference_fieldname, "qty"],
+			as_list=1,
+		)
+	)
+
+	return received_items_map
+
+
 def set_purchase_references(doc):
 	# add internal PO or PR links if any
 	if doc.is_internal_transfer():
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py
index c0005f7..0a765f3 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py
@@ -11,6 +11,7 @@
 			"Payment Request": "reference_name",
 			"Sales Invoice": "return_against",
 			"Auto Repeat": "reference_document",
+			"Purchase Invoice": "inter_company_invoice_reference",
 		},
 		"internal_links": {
 			"Sales Order": ["items", "sales_order"],
@@ -30,5 +31,6 @@
 			{"label": _("Reference"), "items": ["Timesheet", "Delivery Note", "Sales Order"]},
 			{"label": _("Returns"), "items": ["Sales Invoice"]},
 			{"label": _("Subscription"), "items": ["Auto Repeat"]},
+			{"label": _("Internal Transfers"), "items": ["Purchase Invoice"]},
 		],
 	}
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 448ec54..c2e82fb 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -2731,6 +2731,63 @@
 		einvoice = make_einvoice(si)
 		validate_totals(einvoice)
 
+	def test_einvoice_discounts(self):
+		from erpnext.regional.india.e_invoice.utils import make_einvoice, validate_totals
+
+		# Normal Itemized Discount
+		si = get_sales_invoice_for_e_invoice()
+		si.apply_discount_on = ""
+		si.items[0].discount_amount = 4000
+		si.items[1].discount_amount = 300
+		si.save()
+
+		einvoice = make_einvoice(si)
+		validate_totals(einvoice)
+
+		self.assertEqual(einvoice["ItemList"][0]["Discount"], 4000)
+		self.assertEqual(einvoice["ItemList"][1]["Discount"], 300)
+		self.assertEqual(einvoice["ValDtls"]["Discount"], 0)
+
+		# Invoice Discount on net total
+		si = get_sales_invoice_for_e_invoice()
+		si.apply_discount_on = "Net Total"
+		si.discount_amount = 400
+		si.save()
+
+		einvoice = make_einvoice(si)
+		validate_totals(einvoice)
+
+		self.assertEqual(einvoice["ItemList"][0]["Discount"], 316.83)
+		self.assertEqual(einvoice["ItemList"][1]["Discount"], 83.17)
+		self.assertEqual(einvoice["ValDtls"]["Discount"], 0)
+
+		# Invoice Discount on grand total (Itemized Discount)
+		si = get_sales_invoice_for_e_invoice()
+		si.apply_discount_on = "Grand Total"
+		si.discount_amount = 400
+		si.save()
+
+		einvoice = make_einvoice(si)
+		validate_totals(einvoice)
+
+		self.assertEqual(einvoice["ItemList"][0]["Discount"], 268.5)
+		self.assertEqual(einvoice["ItemList"][1]["Discount"], 70.48)
+		self.assertEqual(einvoice["ValDtls"]["Discount"], 0)
+
+		# Invoice Discount on grand total (Cash/Non-Trade Discount)
+		si = get_sales_invoice_for_e_invoice()
+		si.apply_discount_on = "Grand Total"
+		si.is_cash_or_non_trade_discount = 1
+		si.discount_amount = 400
+		si.save()
+
+		einvoice = make_einvoice(si)
+		validate_totals(einvoice)
+
+		self.assertEqual(einvoice["ItemList"][0]["Discount"], 0)
+		self.assertEqual(einvoice["ItemList"][1]["Discount"], 0)
+		self.assertEqual(einvoice["ValDtls"]["Discount"], 400)
+
 	def test_item_tax_net_range(self):
 		item = create_item("T Shirt")
 
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 8daff9d..6be8fd7 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -855,8 +855,8 @@
 	)
 
 	for d in invoice_list:
-		payment_amount = d.invoice_amount - d.outstanding
-		outstanding_amount = d.outstanding
+		payment_amount = d.invoice_amount_in_account_currency - d.outstanding_in_account_currency
+		outstanding_amount = d.outstanding_in_account_currency
 		if outstanding_amount > 0.5 / (10**precision):
 			if (
 				min_outstanding
@@ -872,7 +872,7 @@
 							"voucher_no": d.voucher_no,
 							"voucher_type": d.voucher_type,
 							"posting_date": d.posting_date,
-							"invoice_amount": flt(d.invoice_amount),
+							"invoice_amount": flt(d.invoice_amount_in_account_currency),
 							"payment_amount": payment_amount,
 							"outstanding_amount": outstanding_amount,
 							"due_date": d.due_date,
@@ -1412,7 +1412,7 @@
 						if gle.against_voucher_type
 						else gle.voucher_type,
 						"against_voucher_no": gle.against_voucher if gle.against_voucher else gle.voucher_no,
-						"currency": gle.currency,
+						"account_currency": gle.account_currency,
 						"amount": dr_or_cr,
 						"amount_in_account_currency": dr_or_cr_account_currency,
 						"delinked": True if cancel else False,
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index 2144055..0d8cffe 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -500,6 +500,9 @@
 		else:
 			self.doc.grand_total = flt(self.doc.net_total)
 
+		if self.doc.apply_discount_on == "Grand Total" and self.doc.get("is_cash_or_non_trade_discount"):
+			self.doc.grand_total -= self.doc.discount_amount
+
 		if self.doc.get("taxes"):
 			self.doc.total_taxes_and_charges = flt(
 				self.doc.grand_total - self.doc.net_total - flt(self.doc.rounding_adjustment),
@@ -594,6 +597,12 @@
 			if not self.doc.apply_discount_on:
 				frappe.throw(_("Please select Apply Discount On"))
 
+			if self.doc.apply_discount_on == "Grand Total" and self.doc.get(
+				"is_cash_or_non_trade_discount"
+			):
+				self.discount_amount_applied = True
+				return
+
 			self.doc.base_discount_amount = flt(
 				self.doc.discount_amount * self.doc.conversion_rate, self.doc.precision("base_discount_amount")
 			)
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index e1ccc11..7b7d057 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -624,7 +624,7 @@
 		data = self.get_data_for_eval()
 		for struct_row in self._salary_structure_doc.get(component_type):
 			amount = self.eval_condition_and_formula(struct_row, data)
-			if amount and struct_row.statistical_component == 0:
+			if amount is not None and struct_row.statistical_component == 0:
 				self.update_component_row(struct_row, amount, component_type)
 
 	def get_data_for_eval(self):
@@ -854,6 +854,10 @@
 			component_row, joining_date, relieving_date
 		)[0]
 
+		# remove 0 valued components that have been updated later
+		if component_row.amount == 0:
+			self.remove(component_row)
+
 	def set_precision_for_component_amounts(self):
 		for component_type in ("earnings", "deductions"):
 			for component_row in self.get(component_type):
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 3e36b88..6b618b2 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -1108,9 +1108,25 @@
 		}
 	}
 
+	is_a_mapped_document(item) {
+		const mapped_item_field_map = {
+			"Delivery Note Item": ["si_detail", "so_detail", "dn_detail"],
+			"Sales Invoice Item": ["dn_detail", "so_detail", "sales_invoice_item"],
+			"Purchase Receipt Item": ["purchase_order_item", "purchase_invoice_item", "purchase_receipt_item"],
+			"Purchase Invoice Item": ["purchase_order_item", "pr_detail", "po_detail"],
+		};
+		const mappped_fields = mapped_item_field_map[item.doctype] || [];
+
+		return mappped_fields
+			.map((field) => item[field])
+			.filter(Boolean).length > 0;
+	}
+
 	batch_no(doc, cdt, cdn) {
 		let item = frappe.get_doc(cdt, cdn);
-		this.apply_price_list(item, true);
+		if (!this.is_a_mapped_document(item)) {
+			this.apply_price_list(item, true);
+		}
 	}
 
 	toggle_conversion_factor(item) {
@@ -1484,48 +1500,46 @@
 	}
 
 	_set_values_for_item_list(children) {
-		var me = this;
-		var items_rule_dict = {};
+		const items_rule_dict = {};
 
-		for(var i=0, l=children.length; i<l; i++) {
-			var d = children[i] ;
-			let item_row = frappe.get_doc(d.doctype, d.name);
-			var existing_pricing_rule = frappe.model.get_value(d.doctype, d.name, "pricing_rules");
-			for(var k in d) {
-				var v = d[k];
-				if (["doctype", "name"].indexOf(k)===-1) {
-					if(k=="price_list_rate") {
-						item_row['rate'] = v;
+		for (const child of children) {
+			const existing_pricing_rule = frappe.model.get_value(child.doctype, child.name, "pricing_rules");
+
+			for (const [key, value] of Object.entries(child)) {
+				if (!["doctype", "name"].includes(key)) {
+					if (key === "price_list_rate") {
+						frappe.model.set_value(child.doctype, child.name, "rate", value);
 					}
 
-					if (k !== 'free_item_data') {
-						item_row[k] = v;
+					if (key !== "free_item_data") {
+						frappe.model.set_value(child.doctype, child.name, key, value);
 					}
 				}
 			}
 
-			frappe.model.round_floats_in(item_row, ["price_list_rate", "discount_percentage"]);
+			frappe.model.round_floats_in(
+				frappe.get_doc(child.doctype, child.name),
+				["price_list_rate", "discount_percentage"],
+			);
 
 			// if pricing rule set as blank from an existing value, apply price_list
-			if(!me.frm.doc.ignore_pricing_rule && existing_pricing_rule && !d.pricing_rules) {
-				me.apply_price_list(frappe.get_doc(d.doctype, d.name));
-			} else if(!d.pricing_rules) {
-				me.remove_pricing_rule(frappe.get_doc(d.doctype, d.name));
+			if (!this.frm.doc.ignore_pricing_rule && existing_pricing_rule && !child.pricing_rules) {
+				this.apply_price_list(frappe.get_doc(child.doctype, child.name));
+			} else if (!child.pricing_rules) {
+				this.remove_pricing_rule(frappe.get_doc(child.doctype, child.name));
 			}
 
-			if (d.free_item_data.length > 0) {
-				me.apply_product_discount(d);
+			if (child.free_item_data.length > 0) {
+				this.apply_product_discount(child);
 			}
 
-			if (d.apply_rule_on_other_items) {
-				items_rule_dict[d.name] = d;
+			if (child.apply_rule_on_other_items) {
+				items_rule_dict[child.name] = child;
 			}
 		}
 
-		me.frm.refresh_field('items');
-		me.apply_rule_on_other_items(items_rule_dict);
-
-		me.calculate_taxes_and_totals();
+		this.apply_rule_on_other_items(items_rule_dict);
+		this.calculate_taxes_and_totals();
 	}
 
 	apply_rule_on_other_items(args) {
diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py
index 5eb14a5..fb2779b 100644
--- a/erpnext/regional/india/e_invoice/utils.py
+++ b/erpnext/regional/india/e_invoice/utils.py
@@ -271,14 +271,14 @@
 		item.description = sanitize_for_json(d.item_name)
 
 		item.qty = abs(item.qty)
-		if flt(item.qty) != 0.0:
-			item.unit_rate = abs(item.taxable_value / item.qty)
-		else:
-			item.unit_rate = abs(item.taxable_value)
-		item.gross_amount = abs(item.taxable_value)
-		item.taxable_value = abs(item.taxable_value)
-		item.discount_amount = 0
 
+		if invoice.get("apply_discount_on"):
+			item.discount_amount = item.base_amount - item.base_net_amount
+
+		item.unit_rate = abs(item.taxable_value - item.discount_amount) / item.qty
+
+		item.gross_amount = abs(item.taxable_value) + item.discount_amount
+		item.taxable_value = abs(item.taxable_value)
 		item.is_service_item = "Y" if item.gst_hsn_code and item.gst_hsn_code[:2] == "99" else "N"
 		item.serial_no = ""
 
@@ -352,7 +352,14 @@
 def get_invoice_value_details(invoice):
 	invoice_value_details = frappe._dict(dict())
 	invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get("items")]))
-	invoice_value_details.invoice_discount_amt = 0
+	if (
+		invoice.apply_discount_on == "Grand Total"
+		and invoice.discount_amount
+		and invoice.get("is_cash_or_non_trade_discount")
+	):
+		invoice_value_details.invoice_discount_amt = invoice.discount_amount
+	else:
+		invoice_value_details.invoice_discount_amt = 0
 
 	invoice_value_details.round_off = invoice.base_rounding_adjustment
 	invoice_value_details.base_grand_total = abs(invoice.base_rounded_total) or abs(
diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py
index 0262469..f1586fc 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -1060,8 +1060,16 @@
 				considered_rows.append(prev_row_id)
 
 	for item in doc.get("items"):
-		proportionate_value = item.base_net_amount if doc.base_net_total else item.qty
-		total_value = doc.base_net_total if doc.base_net_total else doc.total_qty
+		if (
+			doc.apply_discount_on == "Grand Total"
+			and doc.discount_amount
+			and doc.get("is_cash_or_non_trade_discount")
+		):
+			proportionate_value = item.base_amount if doc.base_total else item.qty
+			total_value = doc.base_total if doc.base_total else doc.total_qty
+		else:
+			proportionate_value = item.base_net_amount if doc.base_net_total else item.qty
+			total_value = doc.base_net_total if doc.base_net_total else doc.total_qty
 
 		applicable_charges = flt(
 			flt(
diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js
index 0de5b2d..3dec303 100644
--- a/erpnext/setup/doctype/company/company.js
+++ b/erpnext/setup/doctype/company/company.js
@@ -219,8 +219,8 @@
 		["default_discount_account", {}],
 		["discount_allowed_account", {"root_type": "Expense"}],
 		["discount_received_account", {"root_type": "Income"}],
-		["exchange_gain_loss_account", {"root_type": "Expense"}],
-		["unrealized_exchange_gain_loss_account", {"root_type": "Expense"}],
+		["exchange_gain_loss_account", {"root_type": ["in", ["Expense", "Income"]]}],
+		["unrealized_exchange_gain_loss_account", {"root_type": ["in", ["Expense", "Income"]]}],
 		["accumulated_depreciation_account",
 			{"root_type": "Asset", "account_type": "Accumulated Depreciation"}],
 		["depreciation_expense_account", {"root_type": "Expense", "account_type": "Depreciation"}],
diff --git a/erpnext/startup/boot.py b/erpnext/startup/boot.py
index c3445ab..bb120ea 100644
--- a/erpnext/startup/boot.py
+++ b/erpnext/startup/boot.py
@@ -14,7 +14,6 @@
 	if frappe.session["user"] != "Guest":
 		update_page_info(bootinfo)
 
-		load_country_and_currency(bootinfo)
 		bootinfo.sysdefaults.territory = frappe.db.get_single_value("Selling Settings", "territory")
 		bootinfo.sysdefaults.customer_group = frappe.db.get_single_value(
 			"Selling Settings", "customer_group"
@@ -53,20 +52,6 @@
 		bootinfo.party_account_types = frappe._dict(party_account_types)
 
 
-def load_country_and_currency(bootinfo):
-	country = frappe.db.get_default("country")
-	if country and frappe.db.exists("Country", country):
-		bootinfo.docs += [frappe.get_doc("Country", country)]
-
-	bootinfo.docs += frappe.db.sql(
-		"""select name, fraction, fraction_units,
-		number_format, smallest_currency_fraction_value, symbol from tabCurrency
-		where enabled=1""",
-		as_dict=1,
-		update={"doctype": ":Currency"},
-	)
-
-
 def update_page_info(bootinfo):
 	bootinfo.page_info.update(
 		{
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index cef9ddd..84da3cc 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -501,6 +501,7 @@
 				and not d.is_fixed_asset
 				and flt(d.qty)
 				and provisional_accounting_for_non_stock_items
+				and d.get("provisional_expense_account")
 			):
 				self.add_provisional_gl_entry(
 					d, gl_entries, self.posting_date, d.get("provisional_expense_account")
diff --git a/pyproject.toml b/pyproject.toml
index 8043dd9..5acfd39 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,3 +1,35 @@
+[project]
+name = "erpnext"
+authors = [
+    { name = "Frappe Technologies Pvt Ltd", email = "developers@frappe.io"}
+]
+description = "Open Source ERP"
+requires-python = ">=3.10"
+readme = "README.md"
+dynamic = ["version"]
+dependencies = [
+    # Core dependencies
+    "pycountry~=20.7.3",
+    "python-stdnum~=1.16",
+    "Unidecode~=1.2.0",
+    "redisearch~=2.1.0",
+
+    # integration dependencies
+    "gocardless-pro~=1.22.0",
+    "googlemaps",
+    "plaid-python~=7.2.1",
+    "python-youtube~=0.8.0",
+    "taxjar~=1.9.2",
+    "tweepy~=3.10.0",
+]
+
+[build-system]
+requires = ["flit_core >=3.4,<4"]
+build-backend = "flit_core.buildapi"
+
+[tool.bench.dev-dependencies]
+hypothesis = "~=6.31.0"
+
 [tool.black]
 line-length = 99
 
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 83e5375..0000000
--- a/requirements.txt
+++ /dev/null
@@ -1,11 +0,0 @@
-# frappe   # https://github.com/frappe/frappe is installed during bench-init
-gocardless-pro~=1.22.0
-googlemaps
-plaid-python~=7.2.1
-pycountry~=20.7.3
-python-stdnum~=1.16
-python-youtube~=0.8.0
-taxjar~=1.9.2
-tweepy~=3.10.0
-Unidecode~=1.2.0
-redisearch~=2.1.0
diff --git a/setup.py b/setup.py
index 1faff04..0ea4d07 100644
--- a/setup.py
+++ b/setup.py
@@ -1,23 +1,6 @@
-from setuptools import setup, find_packages
-import re, ast
+# TODO: Remove this file when  v15.0.0 is released
+from setuptools import setup
 
-# get version from __version__ variable in erpnext/__init__.py
-_version_re = re.compile(r"__version__\s+=\s+(.*)")
+name = "frappe"
 
-with open("requirements.txt") as f:
-	install_requires = f.read().strip().split("\n")
-
-with open("erpnext/__init__.py", "rb") as f:
-	version = str(ast.literal_eval(_version_re.search(f.read().decode("utf-8")).group(1)))
-
-setup(
-	name="erpnext",
-	version=version,
-	description="Open Source ERP",
-	author="Frappe Technologies",
-	author_email="info@erpnext.com",
-	packages=find_packages(),
-	zip_safe=False,
-	include_package_data=True,
-	install_requires=install_requires,
-)
+setup()