Merge pull request #40260 from ruthra-kumar/support_payment_against_payment

refactor: support payment against reverse payment reconciliation
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index c8a35eb..85e9d16 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -1588,6 +1588,12 @@
 		self.assertEqual(frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount"), -1000)
 		self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 2500)
 
+	def test_zero_qty_return_invoice_with_stock_effect(self):
+		cr_note = create_sales_invoice(qty=-1, rate=300, is_return=1, do_not_submit=True)
+		cr_note.update_stock = True
+		cr_note.items[0].qty = 0
+		self.assertRaises(frappe.ValidationError, cr_note.save)
+
 	def test_return_invoice_with_account_mismatch(self):
 		debtors2 = create_account(
 			parent_account="Accounts Receivable - _TC",
diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
index a0f8af5..de49139 100644
--- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
@@ -182,8 +182,10 @@
 		}
 
 		# check invoice grand total and invoiced column's value for 3 payment terms
-		si = self.create_sales_invoice(no_payment_schedule=True)
-		name = si.name
+		si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
+		si.set_posting_time = True
+		si.posting_date = add_days(today(), -1)
+		si.save().submit()
 
 		report = execute(filters)
 
@@ -207,30 +209,42 @@
 
 		# check invoice grand total, invoiced, paid and outstanding column's value after credit note
 		cr_note = self.create_credit_note(si.name, do_not_submit=True)
-		cr_note.posting_date = add_days(today(), 1)
 		cr_note.update_outstanding_for_self = True
 		cr_note.save().submit()
 		report = execute(filters)
 
 		expected_data_after_credit_note = [
-			[100.0, 100.0, 40.0, 0.0, 60.0, self.debit_to],
-			[0, 0, 100.0, 0.0, -100.0, self.debit_to],
+			[100.0, 100.0, 40.0, 0.0, 60.0, si.name],
+			[0, 0, 100.0, 0.0, -100.0, cr_note.name],
 		]
 		self.assertEqual(len(report[1]), 2)
-		for i in range(2):
-			row = report[1][i - 1]
-			# row = report[1][0]
-			self.assertEqual(
-				expected_data_after_credit_note[i - 1],
-				[
-					row.invoice_grand_total,
-					row.invoiced,
-					row.paid,
-					row.credit_note,
-					row.outstanding,
-					row.party_account,
-				],
-			)
+		si_row = [
+			[
+				row.invoice_grand_total,
+				row.invoiced,
+				row.paid,
+				row.credit_note,
+				row.outstanding,
+				row.voucher_no,
+			]
+			for row in report[1]
+			if row.voucher_no == si.name
+		][0]
+
+		cr_note_row = [
+			[
+				row.invoice_grand_total,
+				row.invoiced,
+				row.paid,
+				row.credit_note,
+				row.outstanding,
+				row.voucher_no,
+			]
+			for row in report[1]
+			if row.voucher_no == cr_note.name
+		][0]
+		self.assertEqual(expected_data_after_credit_note[0], si_row)
+		self.assertEqual(expected_data_after_credit_note[1], cr_note_row)
 
 	def test_payment_againt_po_in_receivable_report(self):
 		"""
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index b7e687d..b16e073 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -169,6 +169,13 @@
 		if not self.get("is_return") and not self.get("is_debit_note"):
 			self.validate_qty_is_not_zero()
 
+		if (
+			self.doctype in ["Sales Invoice", "Purchase Invoice"]
+			and self.get("is_return")
+			and self.get("update_stock")
+		):
+			self.validate_zero_qty_for_return_invoices_with_stock()
+
 		if self.get("_action") and self._action != "update_after_submit":
 			self.set_missing_values(for_validate=True)
 
@@ -603,23 +610,31 @@
 				)
 
 	def validate_due_date(self):
-		if self.get("is_pos"):
+		if self.get("is_pos") or self.doctype not in ["Sales Invoice", "Purchase Invoice"]:
 			return
 
 		from erpnext.accounts.party import validate_due_date
 
-		if self.doctype == "Sales Invoice":
+		posting_date = (
+			self.posting_date if self.doctype == "Sales Invoice" else (self.bill_date or self.posting_date)
+		)
+
+		# skip due date validation for records via Data Import
+		if frappe.flags.in_import and getdate(self.due_date) < getdate(posting_date):
+			self.due_date = posting_date
+
+		elif self.doctype == "Sales Invoice":
 			if not self.due_date:
 				frappe.throw(_("Due Date is mandatory"))
 
 			validate_due_date(
-				self.posting_date,
+				posting_date,
 				self.due_date,
 				self.payment_terms_template,
 			)
 		elif self.doctype == "Purchase Invoice":
 			validate_due_date(
-				self.bill_date or self.posting_date,
+				posting_date,
 				self.due_date,
 				self.bill_date,
 				self.payment_terms_template,
@@ -1045,6 +1060,18 @@
 		else:
 			return flt(args.get(field, 0) / self.get("conversion_rate", 1))
 
+	def validate_zero_qty_for_return_invoices_with_stock(self):
+		rows = []
+		for item in self.items:
+			if not flt(item.qty):
+				rows.append(item)
+		if rows:
+			frappe.throw(
+				_(
+					"For Return Invoices with Stock effect, '0' qty Items are not allowed. Following rows are affected: {0}"
+				).format(frappe.bold(comma_and(["#" + str(x.idx) for x in rows])))
+			)
+
 	def validate_qty_is_not_zero(self):
 		for item in self.items:
 			if self.doctype == "Purchase Receipt" and item.rejected_qty:
@@ -2709,14 +2736,20 @@
 	else:
 		q = q.where(journal_acc.debit_in_account_currency > 0)
 
+	reference_or_condition = []
+
 	if include_unallocated:
-		q = q.where((journal_acc.reference_name.isnull()) | (journal_acc.reference_name == ""))
+		reference_or_condition.append(journal_acc.reference_name.isnull())
+		reference_or_condition.append(journal_acc.reference_name == "")
 
 	if order_list:
-		q = q.where(
+		reference_or_condition.append(
 			(journal_acc.reference_type == order_doctype) & ((journal_acc.reference_name).isin(order_list))
 		)
 
+	if reference_or_condition:
+		q = q.where(Criterion.any(reference_or_condition))
+
 	q = q.orderby(journal_entry.posting_date)
 
 	journal_entries = q.run(as_dict=True)
diff --git a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py
index f2f1e4c..42bdf57 100644
--- a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py
+++ b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/item_group_wise_sales_target_variance.py
@@ -197,6 +197,8 @@
 				):
 					details[p_key] += r.get(qty_or_amount_field, 0)
 					details[variance_key] = details.get(p_key) - details.get(target_key)
+				else:
+					details[variance_key] = details.get(p_key) - details.get(target_key)
 
 			details["total_achieved"] += details.get(p_key)
 			details["total_variance"] = details.get("total_achieved") - details.get("total_target")
@@ -209,31 +211,32 @@
 
 	parent_doc = frappe.qb.DocType(filters.get("doctype"))
 	child_doc = frappe.qb.DocType(filters.get("doctype") + " Item")
-	sales_team = frappe.qb.DocType("Sales Team")
 
-	query = (
-		frappe.qb.from_(parent_doc)
-		.inner_join(child_doc)
-		.on(child_doc.parent == parent_doc.name)
-		.inner_join(sales_team)
-		.on(sales_team.parent == parent_doc.name)
-		.select(
-			child_doc.item_group,
-			(child_doc.stock_qty * sales_team.allocated_percentage / 100).as_("stock_qty"),
-			(child_doc.base_net_amount * sales_team.allocated_percentage / 100).as_("base_net_amount"),
-			sales_team.sales_person,
-			parent_doc[date_field],
-		)
-		.where(
-			(parent_doc.docstatus == 1)
-			& (parent_doc[date_field].between(fiscal_year.year_start_date, fiscal_year.year_end_date))
-		)
-	)
+	query = frappe.qb.from_(parent_doc).inner_join(child_doc).on(child_doc.parent == parent_doc.name)
 
 	if sales_field == "sales_person":
-		query = query.where(sales_team.sales_person.isin(sales_users_or_territory_data))
+		sales_team = frappe.qb.DocType("Sales Team")
+		stock_qty = child_doc.stock_qty * sales_team.allocated_percentage / 100
+		net_amount = child_doc.base_net_amount * sales_team.allocated_percentage / 100
+		sales_field_col = sales_team[sales_field]
+
+		query = query.inner_join(sales_team).on(sales_team.parent == parent_doc.name)
 	else:
-		query = query.where(parent_doc[sales_field].isin(sales_users_or_territory_data))
+		stock_qty = child_doc.stock_qty
+		net_amount = child_doc.base_net_amount
+		sales_field_col = parent_doc[sales_field]
+
+	query = query.select(
+		child_doc.item_group,
+		parent_doc[date_field],
+		(stock_qty).as_("stock_qty"),
+		(net_amount).as_("base_net_amount"),
+		sales_field_col,
+	).where(
+		(parent_doc.docstatus == 1)
+		& (parent_doc[date_field].between(fiscal_year.year_start_date, fiscal_year.year_end_date))
+		& (sales_field_col.isin(sales_users_or_territory_data))
+	)
 
 	return query.run(as_dict=True)
 
diff --git a/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/test_sales_partner_target_variance_based_on_item_group.py b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/test_sales_partner_target_variance_based_on_item_group.py
new file mode 100644
index 0000000..1718668
--- /dev/null
+++ b/erpnext/selling/report/sales_partner_target_variance_based_on_item_group/test_sales_partner_target_variance_based_on_item_group.py
@@ -0,0 +1,57 @@
+import frappe
+from frappe.tests.utils import FrappeTestCase
+from frappe.utils import flt, nowdate
+
+from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+from erpnext.accounts.utils import get_fiscal_year
+from erpnext.selling.report.sales_partner_target_variance_based_on_item_group.sales_partner_target_variance_based_on_item_group import (
+	execute,
+)
+from erpnext.selling.report.sales_person_target_variance_based_on_item_group.test_sales_person_target_variance_based_on_item_group import (
+	create_sales_target_doc,
+	create_target_distribution,
+)
+
+
+class TestSalesPartnerTargetVarianceBasedOnItemGroup(FrappeTestCase):
+	def setUp(self):
+		self.fiscal_year = get_fiscal_year(nowdate())[0]
+
+	def tearDown(self):
+		frappe.db.rollback()
+
+	def test_achieved_target_and_variance_for_partner(self):
+		# Create a Target Distribution
+		distribution = create_target_distribution(self.fiscal_year)
+
+		# Create Sales Partner with targets for the current fiscal year
+		sales_partner = create_sales_target_doc(
+			"Sales Partner", "partner_name", "Sales Partner 1", self.fiscal_year, distribution.name
+		)
+
+		# Create a Sales Invoice for the Partner
+		si = create_sales_invoice(
+			rate=1000,
+			qty=20,
+			do_not_submit=True,
+		)
+		si.sales_partner = sales_partner
+		si.commission_rate = 5
+		si.submit()
+
+		# Check Achieved Target and Variance for the Sales Partner
+		result = execute(
+			frappe._dict(
+				{
+					"fiscal_year": self.fiscal_year,
+					"doctype": "Sales Invoice",
+					"period": "Yearly",
+					"target_on": "Quantity",
+				}
+			)
+		)[1]
+		row = frappe._dict(result[0])
+		self.assertSequenceEqual(
+			[flt(value, 2) for value in (row.total_target, row.total_achieved, row.total_variance)],
+			[50, 20, -30],
+		)
diff --git a/erpnext/selling/report/sales_person_target_variance_based_on_item_group/test_sales_person_target_variance_based_on_item_group.py b/erpnext/selling/report/sales_person_target_variance_based_on_item_group/test_sales_person_target_variance_based_on_item_group.py
index 4ae5d2b..73ae6d0 100644
--- a/erpnext/selling/report/sales_person_target_variance_based_on_item_group/test_sales_person_target_variance_based_on_item_group.py
+++ b/erpnext/selling/report/sales_person_target_variance_based_on_item_group/test_sales_person_target_variance_based_on_item_group.py
@@ -18,17 +18,17 @@
 
 	def test_achieved_target_and_variance(self):
 		# Create a Target Distribution
-		distribution = frappe.new_doc("Monthly Distribution")
-		distribution.distribution_id = "Target Report Distribution"
-		distribution.fiscal_year = self.fiscal_year
-		distribution.get_months()
-		distribution.insert()
+		distribution = create_target_distribution(self.fiscal_year)
 
-		# Create sales people with targets
-		person_1 = create_sales_person_with_target("Sales Person 1", self.fiscal_year, distribution.name)
-		person_2 = create_sales_person_with_target("Sales Person 2", self.fiscal_year, distribution.name)
+		# Create sales people with targets for the current fiscal year
+		person_1 = create_sales_target_doc(
+			"Sales Person", "sales_person_name", "Sales Person 1", self.fiscal_year, distribution.name
+		)
+		person_2 = create_sales_target_doc(
+			"Sales Person", "sales_person_name", "Sales Person 2", self.fiscal_year, distribution.name
+		)
 
-		# Create a Sales Order with 50-50 contribution
+		# Create a Sales Order with 50-50 contribution between both Sales people
 		so = make_sales_order(
 			rate=1000,
 			qty=20,
@@ -69,10 +69,20 @@
 		)
 
 
-def create_sales_person_with_target(sales_person_name, fiscal_year, distribution_id):
-	sales_person = frappe.new_doc("Sales Person")
-	sales_person.sales_person_name = sales_person_name
-	sales_person.append(
+def create_target_distribution(fiscal_year):
+	distribution = frappe.new_doc("Monthly Distribution")
+	distribution.distribution_id = "Target Report Distribution"
+	distribution.fiscal_year = fiscal_year
+	distribution.get_months()
+	return distribution.insert()
+
+
+def create_sales_target_doc(
+	sales_field_dt, sales_field_name, sales_field_value, fiscal_year, distribution_id
+):
+	sales_target_doc = frappe.new_doc(sales_field_dt)
+	sales_target_doc.set(sales_field_name, sales_field_value)
+	sales_target_doc.append(
 		"targets",
 		{
 			"fiscal_year": fiscal_year,
@@ -81,4 +91,6 @@
 			"distribution_id": distribution_id,
 		},
 	)
-	return sales_person.insert()
+	if sales_field_dt == "Sales Partner":
+		sales_target_doc.commission_rate = 5
+	return sales_target_doc.insert()