Merge pull request #31516 from deepeshgarg007/previsional_accounting_report

fix: Incorrect provisional expense booking while reposting
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/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 657cd99..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")
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/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(