import frappe
from frappe import qb
from frappe.utils import add_days, flt, getdate, today

from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.report.accounts_receivable.accounts_receivable import execute
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.controllers.sales_and_purchase_return import make_return_doc
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.tests.utils import ERPNextTestSuite


class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
	def setUp(self):
		self.company = "_Test Company"
		self.company_abbr = "_TC"
		self.customer = "_Test Customer"
		self.item = "_Test Item"
		self.cost_center = "Main - _TC"
		self.warehouse = "Stores - _TC"
		self.income_account = "Sales - _TC"
		self.expense_account = "Cost of Goods Sold - _TC"
		self.debit_to = "Debtors - _TC"
		self.cash = "Cash - _TC"
		self.debtors_usd = "_Test Receivable USD - _TC"

	def create_sales_invoice(self, no_payment_schedule=False, do_not_submit=False, **args):
		frappe.set_user("Administrator")
		si = create_sales_invoice(
			item=self.item,
			company=self.company,
			customer=self.customer,
			debit_to=self.debit_to,
			posting_date=today(),
			parent_cost_center=self.cost_center,
			cost_center=self.cost_center,
			rate=100,
			price_list_rate=100,
			do_not_save=1,
			**args,
		)
		if not no_payment_schedule:
			si.append(
				"payment_schedule",
				dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30),
			)
			si.append(
				"payment_schedule",
				dict(due_date=getdate(add_days(today(), 60)), invoice_portion=50.00, payment_amount=50),
			)
			si.append(
				"payment_schedule",
				dict(due_date=getdate(add_days(today(), 90)), invoice_portion=20.00, payment_amount=20),
			)
		si = si.save()
		if not do_not_submit:
			si = si.submit()
		return si

	def create_payment_entry(self, docname, do_not_submit=False):
		pe = get_payment_entry("Sales Invoice", docname, bank_account=self.cash, party_amount=40)
		pe.paid_from = self.debit_to
		pe.insert()
		if not do_not_submit:
			pe.submit()
		return pe

	def create_credit_note(self, docname, do_not_submit=False):
		credit_note = create_sales_invoice(
			company=self.company,
			customer=self.customer,
			item=self.item,
			qty=-1,
			debit_to=self.debit_to,
			cost_center=self.cost_center,
			is_return=1,
			return_against=docname,
			do_not_submit=do_not_submit,
		)

		return credit_note

	def test_pos_receivable(self):
		filters = {
			"company": self.company,
			"party_type": "Customer",
			"party": [self.customer],
			"report_date": add_days(today(), 2),
			"based_on_payment_terms": 0,
			"range": "30, 60, 90, 120",
			"show_remarks": False,
		}

		pos_inv = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
		pos_inv.posting_date = add_days(today(), 2)
		pos_inv.is_pos = 1
		pos_inv.append(
			"payments",
			frappe._dict(
				mode_of_payment="Cash",
				amount=flt(pos_inv.grand_total / 2),
			),
		)
		pos_inv.disable_rounded_total = 1
		pos_inv.save()
		pos_inv.submit()

		report = execute(filters)
		expected_data = [[pos_inv.grand_total, pos_inv.paid_amount, 0]]

		row = report[1][-1]
		self.assertEqual(expected_data[0], [row.invoiced, row.paid, row.credit_note])
		pos_inv.cancel()

	def test_accounts_receivable_with_payment(self):
		filters = {
			"company": self.company,
			"based_on_payment_terms": 1,
			"report_date": today(),
			"range": "30, 60, 90, 120",
			"show_remarks": True,
		}

		# check invoice grand total and invoiced column's value for 3 payment terms
		si = self.create_sales_invoice()

		report = execute(filters)

		expected_data = [[100, 30], [100, 50], [100, 20]]

		for i in range(3):
			row = report[1][i - 1]
			self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced])
			self.assertFalse(row.get("remarks"))
		# check invoice grand total, invoiced, paid and outstanding column's value after payment
		self.create_payment_entry(si.name)
		report = execute(filters)

		expected_data_after_payment = [[100, 50, 10, 40], [100, 20, 0, 20]]

		for i in range(2):
			row = report[1][i - 1]
			self.assertEqual(
				expected_data_after_payment[i - 1],
				[row.invoice_grand_total, row.invoiced, row.paid, row.outstanding],
			)

		# 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.update_outstanding_for_self = False
		cr_note.save().submit()

		# as the invoice partially paid and returning the full amount so the outstanding amount should be True
		self.assertEqual(cr_note.update_outstanding_for_self, True)

		report = execute(filters)

		expected_data_after_credit_note = [0, 0, 100, 0, -100, self.debit_to]

		row = report[1][-1]
		self.assertEqual(
			expected_data_after_credit_note,
			[
				row.invoice_grand_total,
				row.invoiced,
				row.paid,
				row.credit_note,
				row.outstanding,
				row.party_account,
			],
		)

	def test_accounts_receivable_without_payment(self):
		filters = {
			"company": self.company,
			"based_on_payment_terms": 1,
			"report_date": today(),
			"range": "30, 60, 90, 120",
			"show_remarks": True,
		}

		# check invoice grand total and invoiced column's value for 3 payment terms
		si = self.create_sales_invoice()

		report = execute(filters)

		expected_data = [[100, 30], [100, 50], [100, 20]]

		for i in range(3):
			row = report[1][i - 1]
			self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced])

		# 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.update_outstanding_for_self = False
		cr_note.save().submit()

		self.assertEqual(cr_note.update_outstanding_for_self, False)

		report = execute(filters)

		row = report[1]
		self.assertTrue(len(row) == 0)

	@ERPNextTestSuite.change_settings(
		"Accounts Settings",
		{"allow_multi_currency_invoices_against_single_party_account": 1},
	)
	def test_allow_multi_currency_invoices_against_single_party_account(self):
		filters = {
			"company": self.company,
			"based_on_payment_terms": 1,
			"report_date": today(),
			"range": "30, 60, 90, 120",
			"show_remarks": True,
			"in_party_currency": 1,
		}

		# CASE 1: Company currency and party account currency are the same
		si = self.create_sales_invoice(qty=1, no_payment_schedule=True, do_not_submit=True)
		si.currency = "USD"
		si.conversion_rate = 80
		si.save().submit()

		filters.update(
			{
				"party_type": "Customer",
				"party": [self.customer],
			}
		)
		report = execute(filters)
		row = report[1][0]

		expected_data = [8000, 8000]  # Data in company currency

		self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced])
		self.assertFalse(row.get("remarks"))

		# CASE 2: Transaction currency and party account currency are the same
		self.create_customer(
			"USD Customer", currency="USD", default_account=self.debtors_usd, company=self.company
		)
		si = create_sales_invoice(
			item=self.item,
			company=self.company,
			customer=self.customer,
			debit_to=self.debtors_usd,
			posting_date=today(),
			parent_cost_center=self.cost_center,
			cost_center=self.cost_center,
			rate=100,
			currency="USD",
			conversion_rate=80,
			price_list_rate=100,
			do_not_save=1,
		)
		si.save().submit()

		filters.update(
			{
				"party_type": "Customer",
				"party": [self.customer],
			}
		)
		report = execute(filters)
		row = report[1][0]

		expected_data = [100, 100]  # Data in Part Account Currency

		self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced])
		self.assertFalse(row.get("remarks"))

		# View in Company currency
		filters.pop("in_party_currency")
		report = execute(filters)
		row = report[1][0]

		expected_data = [8000, 8000]  # Data in Company Currency

		self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced])
		self.assertFalse(row.get("remarks"))

	def test_accounts_receivable_with_partial_payment(self):
		filters = {
			"company": self.company,
			"based_on_payment_terms": 1,
			"report_date": today(),
			"range": "30, 60, 90, 120",
			"show_remarks": True,
		}

		# check invoice grand total and invoiced column's value for 3 payment terms
		si = self.create_sales_invoice(qty=2)

		report = execute(filters)

		expected_data = [[200, 60], [200, 100], [200, 40]]

		for i in range(3):
			row = report[1][i - 1]
			self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced])
			self.assertFalse(row.get("remarks"))

		# check invoice grand total, invoiced, paid and outstanding column's value after payment
		self.create_payment_entry(si.name)
		report = execute(filters)

		expected_data_after_payment = [[200, 60, 40, 20], [200, 100, 0, 100], [200, 40, 0, 40]]

		for i in range(3):
			row = report[1][i - 1]
			self.assertEqual(
				expected_data_after_payment[i - 1],
				[row.invoice_grand_total, row.invoiced, row.paid, row.outstanding],
			)

		# 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.update_outstanding_for_self = False
		cr_note.save().submit()

		self.assertFalse(cr_note.update_outstanding_for_self)

		report = execute(filters)

		expected_data_after_credit_note = [
			[200, 100, 0, 80, 20, self.debit_to],
			[200, 40, 0, 0, 40, self.debit_to],
		]

		for i in range(2):
			row = report[1][i - 1]
			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,
				],
			)

	def test_cr_note_flag_to_update_self(self):
		filters = {
			"company": self.company,
			"report_date": today(),
			"range": "30, 60, 90, 120",
			"show_remarks": True,
		}

		# check invoice grand total and invoiced column's value for 3 payment terms
		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)

		expected_data = [100, 100]

		self.assertEqual(len(report[1]), 1)
		row = report[1][0]
		self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced])
		self.assertFalse(row.get("remarks"))

		# check invoice grand total, invoiced, paid and outstanding column's value after payment
		self.create_payment_entry(si.name)
		report = execute(filters)

		expected_data_after_payment = [100, 100, 40, 60]
		self.assertEqual(len(report[1]), 1)
		row = report[1][0]
		self.assertEqual(
			expected_data_after_payment,
			[row.invoice_grand_total, row.invoiced, row.paid, row.outstanding],
		)

		# 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.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, si.name],
			[0, 0, 100.0, 0.0, -100.0, cr_note.name],
		]
		self.assertEqual(len(report[1]), 2)
		si_row = next(
			[
				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
		)

		cr_note_row = next(
			[
				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
		)
		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):
		"""
		Payments made against Purchase Order will show up as outstanding amount
		"""

		so = make_sales_order(
			company=self.company,
			customer=self.customer,
			warehouse=self.warehouse,
			debit_to=self.debit_to,
			income_account=self.income_account,
			expense_account=self.expense_account,
			cost_center=self.cost_center,
		)

		pe = get_payment_entry(so.doctype, so.name)
		pe = pe.save().submit()

		filters = {
			"company": self.company,
			"based_on_payment_terms": 0,
			"report_date": today(),
			"range": "30, 60, 90, 120",
		}

		report = execute(filters)

		expected_data_after_payment = [0, 1000, 0, -1000]

		row = report[1][0]
		self.assertEqual(
			expected_data_after_payment,
			[
				row.invoiced,
				row.paid,
				row.credit_note,
				row.outstanding,
			],
		)

	@ERPNextTestSuite.change_settings(
		"Accounts Settings",
		{"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
	)
	def test_exchange_revaluation_for_party(self):
		"""
		Exchange Revaluation for party on Receivable/Payable should be included
		"""

		# Using Exchange Gain/Loss account for unrealized as well.
		company_doc = frappe.get_doc("Company", self.company)
		company_doc.unrealized_exchange_gain_loss_account = company_doc.exchange_gain_loss_account
		company_doc.save()

		si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
		si.currency = "USD"
		si.conversion_rate = 80
		si.debit_to = self.debtors_usd
		si = si.save().submit()

		# Exchange Revaluation
		err = frappe.new_doc("Exchange Rate Revaluation")
		err.company = self.company
		err.posting_date = today()
		accounts = err.get_accounts_data()
		err.extend("accounts", accounts)
		err.accounts[0].new_exchange_rate = 85
		row = err.accounts[0]
		row.new_balance_in_base_currency = flt(row.new_exchange_rate * flt(row.balance_in_account_currency))
		row.gain_loss = row.new_balance_in_base_currency - flt(row.balance_in_base_currency)
		err.set_total_gain_loss()
		err = err.save().submit()

		# Submit JV for ERR
		err_journals = err.make_jv_entries()
		je = frappe.get_doc("Journal Entry", err_journals.get("revaluation_jv"))
		je = je.submit()

		filters = {
			"company": self.company,
			"report_date": today(),
			"range": "30, 60, 90, 120",
		}
		report = execute(filters)

		expected_data_for_err = [0, -500, 0, 500]
		row = next(x for x in report[1] if x.voucher_type == je.doctype and x.voucher_no == je.name)
		self.assertEqual(
			expected_data_for_err,
			[
				row.invoiced,
				row.paid,
				row.credit_note,
				row.outstanding,
			],
		)

	def test_payment_against_credit_note(self):
		"""
		Payment against credit/debit note should be considered against the parent invoice
		"""

		si1 = self.create_sales_invoice()

		pe = get_payment_entry(si1.doctype, si1.name, bank_account=self.cash)
		pe.paid_from = self.debit_to
		pe.insert()
		pe.submit()

		cr_note = self.create_credit_note(si1.name)

		si2 = self.create_sales_invoice()

		# manually link cr_note with si2 using journal entry
		je = frappe.new_doc("Journal Entry")
		je.company = self.company
		je.voucher_type = "Credit Note"
		je.posting_date = today()

		debit_entry = {
			"account": self.debit_to,
			"party_type": "Customer",
			"party": self.customer,
			"debit": 100,
			"debit_in_account_currency": 100,
			"reference_type": cr_note.doctype,
			"reference_name": cr_note.name,
			"cost_center": self.cost_center,
		}
		credit_entry = {
			"account": self.debit_to,
			"party_type": "Customer",
			"party": self.customer,
			"credit": 100,
			"credit_in_account_currency": 100,
			"reference_type": si2.doctype,
			"reference_name": si2.name,
			"cost_center": self.cost_center,
		}

		je.append("accounts", debit_entry)
		je.append("accounts", credit_entry)
		je = je.save().submit()

		filters = {
			"company": self.company,
			"report_date": today(),
			"range": "30, 60, 90, 120",
		}
		report = execute(filters)
		self.assertEqual(report[1], [])

	def test_group_by_party(self):
		si1 = self.create_sales_invoice(do_not_submit=True)
		si1.posting_date = add_days(today(), -1)
		si1.save().submit()
		si2 = self.create_sales_invoice(do_not_submit=True)
		si2.items[0].rate = 85
		si2.save().submit()

		filters = {
			"company": self.company,
			"report_date": today(),
			"range": "30, 60, 90, 120",
			"group_by_party": True,
		}
		report = execute(filters)[1]
		self.assertEqual(len(report), 5)

		# assert voucher rows
		expected_voucher_rows = [
			[100.0, 100.0, 100.0, 100.0],
			[85.0, 85.0, 85.0, 85.0],
		]
		voucher_rows = []
		for x in report[0:2]:
			voucher_rows.append(
				[x.invoiced, x.outstanding, x.invoiced_in_account_currency, x.outstanding_in_account_currency]
			)
		self.assertEqual(expected_voucher_rows, voucher_rows)

		# assert total rows
		expected_total_rows = [
			[self.customer, 185.0, 185.0],  # party total
			{},  # empty row for padding
			["Total", 185.0, 185.0],  # grand total
		]
		party_total_row = report[2]
		self.assertEqual(
			expected_total_rows[0],
			[
				party_total_row.get("party"),
				party_total_row.get("invoiced"),
				party_total_row.get("outstanding"),
			],
		)
		empty_row = report[3]
		self.assertEqual(expected_total_rows[1], empty_row)
		grand_total_row = report[4]
		self.assertEqual(
			expected_total_rows[2],
			[
				grand_total_row.get("party"),
				grand_total_row.get("invoiced"),
				grand_total_row.get("outstanding"),
			],
		)

	def test_future_payments(self):
		sr = self.create_sales_invoice(do_not_submit=True)
		sr.is_return = 1
		sr.items[0].qty = -1
		sr.items[0].rate = 10
		sr.calculate_taxes_and_totals()
		sr.submit()

		si = self.create_sales_invoice()
		pe = get_payment_entry(si.doctype, si.name)
		pe.append(
			"references",
			{
				"reference_doctype": sr.doctype,
				"reference_name": sr.name,
				"due_date": sr.due_date,
				"total_amount": sr.grand_total,
				"outstanding_amount": sr.outstanding_amount,
				"allocated_amount": sr.outstanding_amount,
			},
		)

		pe.posting_date = add_days(today(), 1)
		pe.paid_amount = 80
		pe.references[0].allocated_amount = 90.0  # pe.paid_amount + sr.grand_total
		pe.save().submit()
		filters = {
			"company": self.company,
			"report_date": today(),
			"range": "30, 60, 90, 120",
			"show_future_payments": True,
		}
		report = execute(filters)[1]
		self.assertEqual(len(report), 2)

		expected_data = {sr.name: [10.0, -10.0, 0.0, -10], si.name: [100.0, 100.0, 10.0, 90.0]}

		rows = report[:2]
		for row in rows:
			self.assertEqual(
				expected_data[row.voucher_no],
				[row.invoiced or row.paid, row.outstanding, row.remaining_balance, row.future_amount],
			)

		pe.cancel()
		sr.load_from_db()  # Outstanding amount is updated so a updated timestamp is needed.
		sr.cancel()

		# full payment in future date
		pe = get_payment_entry(si.doctype, si.name)
		pe.posting_date = add_days(today(), 1)
		pe.save().submit()
		report = execute(filters)[1]
		self.assertEqual(len(report), 1)
		expected_data = [100.0, 100.0, 0.0, 100.0]
		row = report[0]
		self.assertEqual(
			expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
		)

		pe.cancel()
		# over payment in future date
		pe = get_payment_entry(si.doctype, si.name)
		pe.posting_date = add_days(today(), 1)
		pe.paid_amount = 110
		pe.save().submit()
		report = execute(filters)[1]
		self.assertEqual(len(report), 2)
		expected_data = [[100.0, 0.0, 100.0, 0.0, 100.0], [0.0, 10.0, -10.0, -10.0, 0.0]]
		for idx, row in enumerate(report):
			self.assertEqual(
				expected_data[idx],
				[row.invoiced, row.paid, row.outstanding, row.remaining_balance, row.future_amount],
			)

	def test_sales_person(self):
		sales_person = (
			frappe.get_doc({"doctype": "Sales Person", "sales_person_name": "John Clark", "enabled": True})
			.insert()
			.submit()
		)
		si = self.create_sales_invoice(do_not_submit=True)
		si.append("sales_team", {"sales_person": sales_person.name, "allocated_percentage": 100})
		si.save().submit()

		filters = {
			"company": self.company,
			"report_date": today(),
			"range": "30, 60, 90, 120",
			"sales_person": sales_person.name,
			"show_sales_person": True,
		}
		report = execute(filters)[1]
		self.assertEqual(len(report), 1)

		expected_data = [100.0, 100.0, sales_person.name]

		row = report[0]
		self.assertEqual(expected_data, [row.invoiced, row.outstanding, row.sales_person])

	def test_cost_center_filter(self):
		self.create_sales_invoice()
		filters = {
			"company": self.company,
			"report_date": today(),
			"range": "30, 60, 90, 120",
			"cost_center": self.cost_center,
		}
		report = execute(filters)[1]
		self.assertEqual(len(report), 1)
		expected_data = [100.0, 100.0, self.cost_center]
		row = report[0]
		self.assertEqual(expected_data, [row.invoiced, row.outstanding, row.cost_center])

	def test_customer_group_filter(self):
		self.create_sales_invoice()
		cus_group = frappe.db.get_value("Customer", self.customer, "customer_group")
		filters = {
			"company": self.company,
			"report_date": today(),
			"range": "30, 60, 90, 120",
			"customer_group": cus_group,
		}
		report = execute(filters)[1]
		self.assertEqual(len(report), 1)
		expected_data = [100.0, 100.0, cus_group]
		row = report[0]
		self.assertEqual(expected_data, [row.invoiced, row.outstanding, row.customer_group])

		filters.update({"customer_group": "Individual"})
		report = execute(filters)[1]
		self.assertEqual(len(report), 0)

	def test_multi_customer_group_filter(self):
		self.create_sales_invoice()
		cus_group = frappe.db.get_value("Customer", self.customer, "customer_group")
		# Create a list of customer groups, e.g., ["Group1", "Group2"]
		cus_groups_list = [cus_group, "_Test Customer Group 1"]

		filters = {
			"company": self.company,
			"report_date": today(),
			"range": "30, 60, 90, 120",
			"customer_group": cus_groups_list,  # Use the list of customer groups
		}
		report = execute(filters)[1]

		# Assert that the report contains data for the specified customer groups
		self.assertTrue(len(report) > 0)

		for row in report:
			# Assert that the customer group of each row is in the list of customer groups
			self.assertIn(row.customer_group, cus_groups_list)

	def test_territory_filter(self):
		self.create_sales_invoice()
		territory = frappe.db.get_value("Customer", self.customer, "territory")

		filters = {
			"company": self.company,
			"report_date": today(),
			"range": "30, 60, 90, 120",
			"territory": territory,
		}
		report = execute(filters)[1]
		self.assertEqual(len(report), 1)
		self.assertEqual(
			[100.0, 100.0, territory], [report[0].invoiced, report[0].outstanding, report[0].territory]
		)

		filters.update({"territory": ["_Test Territory United States"]})
		self.assertEqual(len(execute(filters)[1]), 0)

		filters.update({"territory": [territory, "_Test Territory United States"]})
		self.assertEqual(len(execute(filters)[1]), 1)

		frappe.db.set_value("Customer", self.customer, "territory", "_Test Territory Maharashtra")
		filters.update({"territory": ["_Test Territory India"]})
		self.assertEqual(len(execute(filters)[1]), 1)

		filters.update({"territory": ["_Test Territory Mars"]})
		self.assertRaises(frappe.ValidationError, execute, filters)

		filters.update({"territory": "  "})
		self.assertRaises(frappe.ValidationError, execute, filters)

	def test_party_account_filter(self):
		si1 = self.create_sales_invoice()
		jane = frappe.get_doc(
			{
				"doctype": "Customer",
				"customer_name": "Jane Doe",
				"type": "Individual",
				"default_currency": "USD",
			}
		).insert()

		self.customer = jane.name
		si2 = self.create_sales_invoice(do_not_submit=True)
		si2.posting_date = add_days(today(), -1)
		si2.currency = "USD"
		si2.conversion_rate = 80
		si2.debit_to = self.debtors_usd
		si2.save().submit()

		# Filter on company currency receivable account
		filters = {
			"company": self.company,
			"report_date": today(),
			"range": "30, 60, 90, 120",
			"party_account": self.debit_to,
		}
		report = execute(filters)[1]
		self.assertEqual(len(report), 1)
		expected_data = [100.0, 100.0, self.debit_to, si1.currency]
		row = report[0]
		self.assertEqual(
			expected_data, [row.invoiced, row.outstanding, row.party_account, row.account_currency]
		)

		# Filter on USD receivable account
		filters.update({"party_account": self.debtors_usd})
		report = execute(filters)[1]
		self.assertEqual(len(report), 1)
		expected_data = [100.0, 100.0, self.debtors_usd, si2.currency]
		row = report[0]
		self.assertEqual(
			expected_data, [row.invoiced, row.outstanding, row.party_account, row.account_currency]
		)

		# without filter on party account
		filters.pop("party_account")
		report = execute(filters)[1]
		self.assertEqual(len(report), 2)
		expected_data = [
			[8000.0, 8000.0, 100.0, 100.0, self.debtors_usd, si2.currency],
			[100.0, 100.0, 100.0, 100.0, self.debit_to, si1.currency],
		]
		for idx, row in enumerate(report):
			self.assertEqual(
				expected_data[idx],
				[
					row.invoiced,
					row.outstanding,
					row.invoiced_in_account_currency,
					row.outstanding_in_account_currency,
					row.party_account,
					row.account_currency,
				],
			)

	def test_usd_customer_filter(self):
		filters = {
			"company": self.company,
			"party_type": "Customer",
			"party": [self.customer],
			"report_date": today(),
			"range": "30, 60, 90, 120",
			"in_party_currency": 1,
		}

		si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
		si.currency = "USD"
		si.conversion_rate = 80
		si.debit_to = self.debtors_usd
		si.save().submit()

		# check invoice grand total and invoiced column's value for 3 payment terms
		report = execute(filters)

		expected = {
			"voucher_type": si.doctype,
			"voucher_no": si.name,
			"party_account": self.debtors_usd,
			"customer_name": self.customer,
			"invoiced": 100.0,
			"outstanding": 100.0,
			"account_currency": "USD",
		}
		self.assertEqual(len(report[1]), 1)
		report_output = report[1][0]
		for field in expected:
			with self.subTest(field=field):
				self.assertEqual(report_output.get(field), expected.get(field))

	def test_multi_select_party_filter(self):
		self.customer1 = self.customer
		self.create_customer("_Test Customer 2")
		self.customer2 = self.customer
		self.create_customer("_Test Customer 3")
		self.customer3 = self.customer

		filters = {
			"company": self.company,
			"party_type": "Customer",
			"party": [self.customer1, self.customer3],
			"report_date": today(),
			"range": "30, 60, 90, 120",
		}

		si1 = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
		si1.customer = self.customer1
		si1.save().submit()

		si2 = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
		si2.customer = self.customer2
		si2.save().submit()

		si3 = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
		si3.customer = self.customer3
		si3.save().submit()

		# check invoice grand total and invoiced column's value for 3 payment terms
		report = execute(filters)

		expected_output = {self.customer1, self.customer3}
		self.assertEqual(len(report[1]), 2)
		output_for = set([x.party for x in report[1]])
		self.assertEqual(output_for, expected_output)

	def test_report_output_if_party_is_missing(self):
		acc_name = "Additional Debtors"
		if not frappe.db.get_value("Account", filters={"account_name": acc_name, "company": self.company}):
			additional_receivable_acc = frappe.get_doc(
				{
					"doctype": "Account",
					"account_name": acc_name,
					"parent_account": "Accounts Receivable - " + self.company_abbr,
					"company": self.company,
					"account_type": "Receivable",
				}
			).save()
			self.debtors2 = additional_receivable_acc.name

		je = frappe.new_doc("Journal Entry")
		je.company = self.company
		je.posting_date = today()
		je.append(
			"accounts",
			{
				"account": self.debit_to,
				"party_type": "Customer",
				"party": self.customer,
				"debit_in_account_currency": 150,
				"credit_in_account_currency": 0,
				"cost_center": self.cost_center,
			},
		)
		je.append(
			"accounts",
			{
				"account": self.debtors2,
				"party_type": "Customer",
				"party": self.customer,
				"debit_in_account_currency": 200,
				"credit_in_account_currency": 0,
				"cost_center": self.cost_center,
			},
		)
		je.append(
			"accounts",
			{
				"account": self.cash,
				"debit_in_account_currency": 0,
				"credit_in_account_currency": 350,
				"cost_center": self.cost_center,
			},
		)
		je.save().submit()

		# manually remove party from Payment Ledger
		ple = qb.DocType("Payment Ledger Entry")
		qb.update(ple).set(ple.party, None).where(ple.voucher_no == je.name).run()

		filters = {
			"company": self.company,
			"report_date": today(),
			"range": "30, 60, 90, 120",
		}

		report_ouput = execute(filters)[1]
		expected_data = [
			[self.debtors2, je.doctype, je.name, "Customer", self.customer, 200.0, 0.0, 0.0, 200.0],
			[self.debit_to, je.doctype, je.name, "Customer", self.customer, 150.0, 0.0, 0.0, 150.0],
		]
		self.assertEqual(len(report_ouput), 2)
		# fetch only required fields
		report_output = [
			[
				x.party_account,
				x.voucher_type,
				x.voucher_no,
				"Customer",
				self.customer,
				x.invoiced,
				x.paid,
				x.credit_note,
				x.outstanding,
			]
			for x in report_ouput
		]
		# use account name to sort
		# post sorting output should be [[Additional Debtors, ...], [Debtors, ...]]
		report_output = sorted(report_output, key=lambda x: x[0])
		self.assertEqual(expected_data, report_output)

	def test_future_payments_on_foreign_currency(self):
		jane = frappe.get_doc(
			{
				"doctype": "Customer",
				"customer_name": "Jane Doe",
				"type": "Individual",
				"default_currency": "USD",
			}
		).insert()
		self.customer = jane.name

		si = self.create_sales_invoice(do_not_submit=True)
		si.posting_date = add_days(today(), -1)
		si.currency = "USD"
		si.conversion_rate = 80
		si.debit_to = self.debtors_usd
		si.save().submit()

		# full payment in USD
		pe = get_payment_entry(si.doctype, si.name)
		pe.posting_date = add_days(today(), 1)
		pe.base_received_amount = 7500
		pe.received_amount = 7500
		pe.source_exchange_rate = 75
		pe.save().submit()

		filters = frappe._dict(
			{
				"company": self.company,
				"report_date": today(),
				"range": "30, 60, 90, 120",
				"show_future_payments": True,
				"in_party_currency": False,
			}
		)
		report = execute(filters)[1]
		self.assertEqual(len(report), 1)

		expected_data = [8000.0, 8000.0, 500.0, 7500.0]
		row = report[0]
		self.assertEqual(
			expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
		)

		filters.in_party_currency = True
		report = execute(filters)[1]
		self.assertEqual(len(report), 1)
		expected_data = [100.0, 100.0, 0.0, 100.0]
		row = report[0]
		self.assertEqual(
			expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
		)

		pe.cancel()
		# partial payment in USD on a future date
		pe = get_payment_entry(si.doctype, si.name)
		pe.posting_date = add_days(today(), 1)
		pe.base_received_amount = 6750
		pe.received_amount = 6750
		pe.source_exchange_rate = 75
		pe.paid_amount = 90  # in USD
		pe.references[0].allocated_amount = 90
		pe.save().submit()

		filters.in_party_currency = False
		report = execute(filters)[1]
		self.assertEqual(len(report), 1)
		expected_data = [8000.0, 8000.0, 1250.0, 6750.0]
		row = report[0]
		self.assertEqual(
			expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
		)

		filters.in_party_currency = True
		report = execute(filters)[1]
		self.assertEqual(len(report), 1)
		expected_data = [100.0, 100.0, 10.0, 90.0]
		row = report[0]
		self.assertEqual(
			expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
		)

	def test_accounts_receivable_output_for_minor_outstanding(self):
		"""
		AR/AP should report miniscule outstanding of 0.01. Or else there will be slight difference with General Ledger/Trial Balance
		"""
		filters = {
			"company": self.company,
			"report_date": today(),
			"range": "30, 60, 90, 120",
		}

		# check invoice grand total and invoiced column's value for 3 payment terms
		si = self.create_sales_invoice(no_payment_schedule=True)

		pe = get_payment_entry("Sales Invoice", si.name, bank_account=self.cash, party_amount=99.99)
		pe.paid_from = self.debit_to
		pe.save().submit()
		report = execute(filters)

		expected_data_after_payment = [100, 100, 99.99, 0.01]
		self.assertEqual(len(report[1]), 1)
		row = report[1][0]
		self.assertEqual(
			expected_data_after_payment,
			[row.invoice_grand_total, row.invoiced, row.paid, row.outstanding],
		)

	def test_cost_center_on_report_output(self):
		filters = {
			"company": self.company,
			"report_date": today(),
			"range": "30, 60, 90, 120",
		}

		# check invoice grand total and invoiced column's value for 3 payment terms
		si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
		si.cost_center = self.cost_center
		si.save().submit()

		new_cc = frappe.get_doc(
			{
				"doctype": "Cost Center",
				"cost_center_name": "East Wing",
				"parent_cost_center": self.company + " - " + self.company_abbr,
				"company": self.company,
			}
		)
		new_cc.save()

		# check invoice grand total, invoiced, paid and outstanding column's value after payment
		pe = self.create_payment_entry(si.name, do_not_submit=True)
		pe.cost_center = new_cc.name
		pe.save().submit()
		report = execute(filters)

		expected_data_after_payment = [si.name, si.cost_center, 60]

		self.assertEqual(len(report[1]), 1)
		row = report[1][0]
		self.assertEqual(expected_data_after_payment, [row.voucher_no, row.cost_center, row.outstanding])

	def test_payment_terms_template_filters(self):
		from erpnext.controllers.accounts_controller import get_payment_terms

		payment_term1 = frappe.get_doc(
			{"doctype": "Payment Term", "payment_term_name": "_Test 50% on 15 Days"}
		).insert()
		payment_term2 = frappe.get_doc(
			{"doctype": "Payment Term", "payment_term_name": "_Test 50% on 30 Days"}
		).insert()

		template = frappe.get_doc(
			{
				"doctype": "Payment Terms Template",
				"template_name": "_Test 50-50",
				"terms": [
					{
						"doctype": "Payment Terms Template Detail",
						"due_date_based_on": "Day(s) after invoice date",
						"payment_term": payment_term1.name,
						"description": "_Test 50-50",
						"invoice_portion": 50,
						"credit_days": 15,
					},
					{
						"doctype": "Payment Terms Template Detail",
						"due_date_based_on": "Day(s) after invoice date",
						"payment_term": payment_term2.name,
						"description": "_Test 50-50",
						"invoice_portion": 50,
						"credit_days": 30,
					},
				],
			}
		)
		template.insert()

		filters = {
			"company": self.company,
			"report_date": today(),
			"range": "30, 60, 90, 120",
			"based_on_payment_terms": 1,
			"payment_terms_template": template.name,
			"ageing_based_on": "Posting Date",
		}

		si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
		si.payment_terms_template = template.name
		schedule = get_payment_terms(template.name)
		si.set("payment_schedule", [])

		for row in schedule:
			row["due_date"] = add_days(si.posting_date, row.get("credit_days", 0))
			si.append("payment_schedule", row)

		si.save()
		si.submit()

		report = execute(filters)
		row = report[1][0]

		self.assertEqual(len(report[1]), 2)
		self.assertEqual([si.name, payment_term1.payment_term_name], [row.voucher_no, row.payment_term])

	def test_project_filter(self):
		project = frappe.get_doc(
			{"doctype": "Project", "project_name": "_Test AR Project", "company": self.company}
		).insert()

		si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
		si.project = project.name
		si.save().submit()

		filters = {
			"company": self.company,
			"report_date": today(),
			"range": "30, 60, 90, 120",
			"project": [project.name],
		}

		report = execute(filters)[1]
		self.assertEqual(len(report), 1)
		row = report[0]
		self.assertEqual(row.project, project.name)
		self.assertEqual(row.invoiced, 100.0)

	def test_project_on_report_output(self):
		"""
		Report row must carry the invoice's project even when the payment entry
		has no project set.
		"""
		filters = {
			"company": self.company,
			"report_date": today(),
			"range": "30, 60, 90, 120",
		}

		project = frappe.get_doc(
			{"doctype": "Project", "project_name": "_Test AR Project Output", "company": self.company}
		).insert()

		si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
		si.project = project.name
		si.save().submit()

		# payment has no project — report row must still show the invoice's project
		self.create_payment_entry(si.name)
		report = execute(filters)

		self.assertEqual(len(report[1]), 1)
		row = report[1][0]
		self.assertEqual([si.name, project.name, 60], [row.voucher_no, row.project, row.outstanding])

	def test_accounts_receivable_respects_user_permissions(self):
		# Party is a dynamic link on Payment Ledger Entry, so user permissions on Customer
		# must be applied explicitly. The report should only show permitted customers.
		original_customer = self.customer
		second_customer = "_Test AR Perm Customer"

		# create_customer overrides self.customer, so build the restricted invoice first
		self.create_customer(customer_name=second_customer)
		self.create_sales_invoice(no_payment_schedule=True)

		self.customer = original_customer
		allowed_invoice = self.create_sales_invoice(no_payment_schedule=True)

		test_user = "test_ar_user_permission@example.com"
		if not frappe.db.exists("User", test_user):
			user = frappe.new_doc("User")
			user.email = test_user
			user.first_name = "AR Perm"
			user.append("roles", {"role": "Accounts User"})
			user.save()

		frappe.permissions.add_user_permission("Customer", original_customer, test_user)

		filters = {
			"company": self.company,
			"party_type": "Customer",
			"report_date": today(),
			"range": "30, 60, 90, 120",
		}

		frappe.set_user(test_user)
		try:
			report = execute(filters)
		finally:
			frappe.set_user("Administrator")

		parties = {row.party for row in report[1]}
		self.assertIn(original_customer, parties)
		self.assertNotIn(second_customer, parties)
		self.assertEqual(allowed_invoice.customer, original_customer)

	def test_receivable_filtered_by_sales_partner(self):
		frappe.set_user("Administrator")
		partner_a, partner_b = "_Test AR Sales Partner A", "_Test AR Sales Partner B"
		for partner in (partner_a, partner_b):
			if not frappe.db.exists("Sales Partner", partner):
				frappe.get_doc(
					{
						"doctype": "Sales Partner",
						"partner_name": partner,
						"commission_rate": 0,
						"territory": "All Territories",
					}
				).insert()

		def _si(sales_partner):
			si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True, qty=2)
			si.sales_partner = sales_partner
			return si.save().submit()

		partner_a_si = _si(partner_a)
		partner_b_si = _si(partner_b)
		no_partner_si = _si(None)

		# a return is folded onto the invoice it settles, so it nets against that
		# invoice's partner even when the return's own partner is cleared
		no_partner_return = make_return_doc("Sales Invoice", partner_a_si.name)
		no_partner_return.sales_partner = None
		no_partner_return.items[0].qty = -1
		no_partner_return.update_outstanding_for_self = 0
		no_partner_return.save().submit()

		filters = {
			"company": self.company,
			"party_type": "Customer",
			"report_date": today(),
			"range": "30, 60, 90, 120",
		}

		def rows_for(partner):
			return {
				r.voucher_no: r
				for r in execute({**filters, "sales_partner": partner})[1]
				if r.get("voucher_no")
			}

		rows_a = rows_for(partner_a)
		self.assertIn(partner_a_si.name, rows_a)
		self.assertEqual(rows_a[partner_a_si.name].sales_partner, partner_a)
		self.assertNotIn(partner_b_si.name, rows_a)
		self.assertNotIn(no_partner_si.name, rows_a)
		self.assertNotIn(no_partner_return.name, rows_a)
		self.assertEqual(rows_a[partner_a_si.name].credit_note, 100)
		self.assertEqual(rows_a[partner_a_si.name].outstanding, 100)

		rows_b = rows_for(partner_b)
		self.assertIn(partner_b_si.name, rows_b)
		self.assertNotIn(partner_a_si.name, rows_b)
