1191 lines
		
	
	
		
			40 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			1191 lines
		
	
	
		
			40 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
 | |
| # For license information, please see license.txt
 | |
| 
 | |
| import unittest
 | |
| 
 | |
| import frappe
 | |
| from frappe import qb
 | |
| from frappe.query_builder.functions import Sum
 | |
| from frappe.tests.utils import FrappeTestCase, change_settings
 | |
| from frappe.utils import add_days, flt, nowdate
 | |
| 
 | |
| from erpnext import get_default_cost_center
 | |
| from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
 | |
| from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
 | |
| from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
 | |
| from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
 | |
| from erpnext.accounts.party import get_party_account
 | |
| from erpnext.stock.doctype.item.test_item import create_item
 | |
| 
 | |
| 
 | |
| def make_customer(customer_name, currency=None):
 | |
| 	if not frappe.db.exists("Customer", customer_name):
 | |
| 		customer = frappe.new_doc("Customer")
 | |
| 		customer.customer_name = customer_name
 | |
| 		customer.customer_type = "Individual"
 | |
| 
 | |
| 		if currency:
 | |
| 			customer.default_currency = currency
 | |
| 		customer.save()
 | |
| 		return customer.name
 | |
| 	else:
 | |
| 		return customer_name
 | |
| 
 | |
| 
 | |
| def make_supplier(supplier_name, currency=None):
 | |
| 	if not frappe.db.exists("Supplier", supplier_name):
 | |
| 		supplier = frappe.new_doc("Supplier")
 | |
| 		supplier.supplier_name = supplier_name
 | |
| 		supplier.supplier_type = "Individual"
 | |
| 		supplier.supplier_group = "All Supplier Groups"
 | |
| 
 | |
| 		if currency:
 | |
| 			supplier.default_currency = currency
 | |
| 		supplier.save()
 | |
| 		return supplier.name
 | |
| 	else:
 | |
| 		return supplier_name
 | |
| 
 | |
| 
 | |
| class TestAccountsController(FrappeTestCase):
 | |
| 	"""
 | |
| 	Test Exchange Gain/Loss booking on various scenarios.
 | |
| 	Test Cases are numbered for better organization
 | |
| 
 | |
| 	10 series - Sales Invoice against Payment Entries
 | |
| 	20 series - Sales Invoice against Journals
 | |
| 	30 series - Sales Invoice against Credit Notes
 | |
| 	40 series - Company default Cost center is unset
 | |
| 	"""
 | |
| 
 | |
| 	def setUp(self):
 | |
| 		self.create_company()
 | |
| 		self.create_account()
 | |
| 		self.create_item()
 | |
| 		self.create_parties()
 | |
| 		self.clear_old_entries()
 | |
| 
 | |
| 	def tearDown(self):
 | |
| 		frappe.db.rollback()
 | |
| 
 | |
| 	def create_company(self):
 | |
| 		company_name = "_Test Company"
 | |
| 		self.company_abbr = abbr = "_TC"
 | |
| 		if frappe.db.exists("Company", company_name):
 | |
| 			company = frappe.get_doc("Company", company_name)
 | |
| 		else:
 | |
| 			company = frappe.get_doc(
 | |
| 				{
 | |
| 					"doctype": "Company",
 | |
| 					"company_name": company_name,
 | |
| 					"country": "India",
 | |
| 					"default_currency": "INR",
 | |
| 					"create_chart_of_accounts_based_on": "Standard Template",
 | |
| 					"chart_of_accounts": "Standard",
 | |
| 				}
 | |
| 			)
 | |
| 			company = company.save()
 | |
| 
 | |
| 		self.company = company.name
 | |
| 		self.cost_center = company.cost_center
 | |
| 		self.warehouse = "Stores - " + abbr
 | |
| 		self.finished_warehouse = "Finished Goods - " + abbr
 | |
| 		self.income_account = "Sales - " + abbr
 | |
| 		self.expense_account = "Cost of Goods Sold - " + abbr
 | |
| 		self.debit_to = "Debtors - " + abbr
 | |
| 		self.debit_usd = "Debtors USD - " + abbr
 | |
| 		self.cash = "Cash - " + abbr
 | |
| 		self.creditors = "Creditors - " + abbr
 | |
| 
 | |
| 	def create_item(self):
 | |
| 		item = create_item(
 | |
| 			item_code="_Test Notebook", is_stock_item=0, company=self.company, warehouse=self.warehouse
 | |
| 		)
 | |
| 		self.item = item if isinstance(item, str) else item.item_code
 | |
| 
 | |
| 	def create_parties(self):
 | |
| 		self.create_customer()
 | |
| 		self.create_supplier()
 | |
| 
 | |
| 	def create_customer(self):
 | |
| 		self.customer = make_customer("_Test MC Customer USD", "USD")
 | |
| 
 | |
| 	def create_supplier(self):
 | |
| 		self.supplier = make_supplier("_Test MC Supplier USD", "USD")
 | |
| 
 | |
| 	def create_account(self):
 | |
| 		account_name = "Debtors USD"
 | |
| 		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 - " + self.company_abbr
 | |
| 			acc.company = self.company
 | |
| 			acc.account_currency = "USD"
 | |
| 			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_usd = acc.name
 | |
| 
 | |
| 	def create_sales_invoice(
 | |
| 		self,
 | |
| 		qty=1,
 | |
| 		rate=1,
 | |
| 		conversion_rate=80,
 | |
| 		posting_date=nowdate(),
 | |
| 		do_not_save=False,
 | |
| 		do_not_submit=False,
 | |
| 	):
 | |
| 		"""
 | |
| 		Helper function to populate default values in sales invoice
 | |
| 		"""
 | |
| 		sinv = create_sales_invoice(
 | |
| 			qty=qty,
 | |
| 			rate=rate,
 | |
| 			company=self.company,
 | |
| 			customer=self.customer,
 | |
| 			item_code=self.item,
 | |
| 			item_name=self.item,
 | |
| 			cost_center=self.cost_center,
 | |
| 			warehouse=self.warehouse,
 | |
| 			debit_to=self.debit_usd,
 | |
| 			parent_cost_center=self.cost_center,
 | |
| 			update_stock=0,
 | |
| 			currency="USD",
 | |
| 			conversion_rate=conversion_rate,
 | |
| 			is_pos=0,
 | |
| 			is_return=0,
 | |
| 			return_against=None,
 | |
| 			income_account=self.income_account,
 | |
| 			expense_account=self.expense_account,
 | |
| 			do_not_save=do_not_save,
 | |
| 			do_not_submit=do_not_submit,
 | |
| 		)
 | |
| 		return sinv
 | |
| 
 | |
| 	def create_payment_entry(
 | |
| 		self, amount=1, source_exc_rate=75, posting_date=nowdate(), customer=None
 | |
| 	):
 | |
| 		"""
 | |
| 		Helper function to populate default values in payment entry
 | |
| 		"""
 | |
| 		payment = create_payment_entry(
 | |
| 			company=self.company,
 | |
| 			payment_type="Receive",
 | |
| 			party_type="Customer",
 | |
| 			party=customer or self.customer,
 | |
| 			paid_from=self.debit_usd,
 | |
| 			paid_to=self.cash,
 | |
| 			paid_amount=amount,
 | |
| 		)
 | |
| 		payment.source_exchange_rate = source_exc_rate
 | |
| 		payment.received_amount = source_exc_rate * amount
 | |
| 		payment.posting_date = posting_date
 | |
| 		return payment
 | |
| 
 | |
| 	def clear_old_entries(self):
 | |
| 		doctype_list = [
 | |
| 			"GL Entry",
 | |
| 			"Payment Ledger Entry",
 | |
| 			"Sales Invoice",
 | |
| 			"Purchase Invoice",
 | |
| 			"Payment Entry",
 | |
| 			"Journal Entry",
 | |
| 		]
 | |
| 		for doctype in doctype_list:
 | |
| 			qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
 | |
| 
 | |
| 	def create_payment_reconciliation(self):
 | |
| 		pr = frappe.new_doc("Payment Reconciliation")
 | |
| 		pr.company = self.company
 | |
| 		pr.party_type = "Customer"
 | |
| 		pr.party = self.customer
 | |
| 		pr.receivable_payable_account = get_party_account(pr.party_type, pr.party, pr.company)
 | |
| 		pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate()
 | |
| 		return pr
 | |
| 
 | |
| 	def create_journal_entry(
 | |
| 		self,
 | |
| 		acc1=None,
 | |
| 		acc1_exc_rate=None,
 | |
| 		acc2_exc_rate=None,
 | |
| 		acc2=None,
 | |
| 		acc1_amount=0,
 | |
| 		acc2_amount=0,
 | |
| 		posting_date=None,
 | |
| 		cost_center=None,
 | |
| 	):
 | |
| 		je = frappe.new_doc("Journal Entry")
 | |
| 		je.posting_date = posting_date or nowdate()
 | |
| 		je.company = self.company
 | |
| 		je.user_remark = "test"
 | |
| 		je.multi_currency = True
 | |
| 		if not cost_center:
 | |
| 			cost_center = self.cost_center
 | |
| 		je.set(
 | |
| 			"accounts",
 | |
| 			[
 | |
| 				{
 | |
| 					"account": acc1,
 | |
| 					"exchange_rate": acc1_exc_rate or 1,
 | |
| 					"cost_center": cost_center,
 | |
| 					"debit_in_account_currency": acc1_amount if acc1_amount > 0 else 0,
 | |
| 					"credit_in_account_currency": abs(acc1_amount) if acc1_amount < 0 else 0,
 | |
| 					"debit": acc1_amount * acc1_exc_rate if acc1_amount > 0 else 0,
 | |
| 					"credit": abs(acc1_amount * acc1_exc_rate) if acc1_amount < 0 else 0,
 | |
| 				},
 | |
| 				{
 | |
| 					"account": acc2,
 | |
| 					"exchange_rate": acc2_exc_rate or 1,
 | |
| 					"cost_center": cost_center,
 | |
| 					"credit_in_account_currency": acc2_amount if acc2_amount > 0 else 0,
 | |
| 					"debit_in_account_currency": abs(acc2_amount) if acc2_amount < 0 else 0,
 | |
| 					"credit": acc2_amount * acc2_exc_rate if acc2_amount > 0 else 0,
 | |
| 					"debit": abs(acc2_amount * acc2_exc_rate) if acc2_amount < 0 else 0,
 | |
| 				},
 | |
| 			],
 | |
| 		)
 | |
| 		return je
 | |
| 
 | |
| 	def get_journals_for(self, voucher_type: str, voucher_no: str) -> list:
 | |
| 		journals = []
 | |
| 		if voucher_type and voucher_no:
 | |
| 			journals = frappe.db.get_all(
 | |
| 				"Journal Entry Account",
 | |
| 				filters={"reference_type": voucher_type, "reference_name": voucher_no, "docstatus": 1},
 | |
| 				fields=["parent"],
 | |
| 			)
 | |
| 		return journals
 | |
| 
 | |
| 	def assert_ledger_outstanding(
 | |
| 		self,
 | |
| 		voucher_type: str,
 | |
| 		voucher_no: str,
 | |
| 		outstanding: float,
 | |
| 		outstanding_in_account_currency: float,
 | |
| 	) -> None:
 | |
| 		"""
 | |
| 		Assert outstanding amount based on ledger on both company/base currency and account currency
 | |
| 		"""
 | |
| 
 | |
| 		ple = qb.DocType("Payment Ledger Entry")
 | |
| 		current_outstanding = (
 | |
| 			qb.from_(ple)
 | |
| 			.select(
 | |
| 				Sum(ple.amount).as_("outstanding"),
 | |
| 				Sum(ple.amount_in_account_currency).as_("outstanding_in_account_currency"),
 | |
| 			)
 | |
| 			.where(
 | |
| 				(ple.against_voucher_type == voucher_type)
 | |
| 				& (ple.against_voucher_no == voucher_no)
 | |
| 				& (ple.delinked == 0)
 | |
| 			)
 | |
| 			.run(as_dict=True)[0]
 | |
| 		)
 | |
| 		self.assertEqual(outstanding, current_outstanding.outstanding)
 | |
| 		self.assertEqual(
 | |
| 			outstanding_in_account_currency, current_outstanding.outstanding_in_account_currency
 | |
| 		)
 | |
| 
 | |
| 	def test_10_payment_against_sales_invoice(self):
 | |
| 		# Sales Invoice in Foreign Currency
 | |
| 		rate = 80
 | |
| 		rate_in_account_currency = 1
 | |
| 
 | |
| 		si = self.create_sales_invoice(qty=1, rate=rate_in_account_currency)
 | |
| 
 | |
| 		# Test payments with different exchange rates
 | |
| 		for exc_rate in [75.9, 83.1, 80.01]:
 | |
| 			with self.subTest(exc_rate=exc_rate):
 | |
| 				pe = self.create_payment_entry(amount=1, source_exc_rate=exc_rate).save()
 | |
| 				pe.append(
 | |
| 					"references",
 | |
| 					{"reference_doctype": si.doctype, "reference_name": si.name, "allocated_amount": 1},
 | |
| 				)
 | |
| 				pe = pe.save().submit()
 | |
| 
 | |
| 				# Outstanding in both currencies should be '0'
 | |
| 				si.reload()
 | |
| 				self.assertEqual(si.outstanding_amount, 0)
 | |
| 				self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
 | |
| 
 | |
| 				# Exchange Gain/Loss Journal should've been created.
 | |
| 				exc_je_for_si = self.get_journals_for(si.doctype, si.name)
 | |
| 				exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
 | |
| 				self.assertNotEqual(exc_je_for_si, [])
 | |
| 				self.assertEqual(len(exc_je_for_si), 1)
 | |
| 				self.assertEqual(len(exc_je_for_pe), 1)
 | |
| 				self.assertEqual(exc_je_for_si[0], exc_je_for_pe[0])
 | |
| 
 | |
| 				# Cancel Payment
 | |
| 				pe.cancel()
 | |
| 
 | |
| 				# outstanding should be same as grand total
 | |
| 				si.reload()
 | |
| 				self.assertEqual(si.outstanding_amount, rate_in_account_currency)
 | |
| 				self.assert_ledger_outstanding(si.doctype, si.name, rate, rate_in_account_currency)
 | |
| 
 | |
| 				# Exchange Gain/Loss Journal should've been cancelled
 | |
| 				exc_je_for_si = self.get_journals_for(si.doctype, si.name)
 | |
| 				exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
 | |
| 				self.assertEqual(exc_je_for_si, [])
 | |
| 				self.assertEqual(exc_je_for_pe, [])
 | |
| 
 | |
| 	def test_11_advance_against_sales_invoice(self):
 | |
| 		# Advance Payment
 | |
| 		adv = self.create_payment_entry(amount=1, source_exc_rate=85).save().submit()
 | |
| 		adv.reload()
 | |
| 
 | |
| 		# Sales Invoices in different exchange rates
 | |
| 		for exc_rate in [75.9, 83.1, 80.01]:
 | |
| 			with self.subTest(exc_rate=exc_rate):
 | |
| 				si = self.create_sales_invoice(qty=1, conversion_rate=exc_rate, rate=1, do_not_submit=True)
 | |
| 				advances = si.get_advance_entries()
 | |
| 				self.assertEqual(len(advances), 1)
 | |
| 				self.assertEqual(advances[0].reference_name, adv.name)
 | |
| 				si.append(
 | |
| 					"advances",
 | |
| 					{
 | |
| 						"doctype": "Sales Invoice Advance",
 | |
| 						"reference_type": advances[0].reference_type,
 | |
| 						"reference_name": advances[0].reference_name,
 | |
| 						"reference_row": advances[0].reference_row,
 | |
| 						"advance_amount": 1,
 | |
| 						"allocated_amount": 1,
 | |
| 						"ref_exchange_rate": advances[0].exchange_rate,
 | |
| 						"remarks": advances[0].remarks,
 | |
| 					},
 | |
| 				)
 | |
| 
 | |
| 				si = si.save()
 | |
| 				si = si.submit()
 | |
| 
 | |
| 				# Outstanding in both currencies should be '0'
 | |
| 				adv.reload()
 | |
| 				self.assertEqual(si.outstanding_amount, 0)
 | |
| 				self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
 | |
| 
 | |
| 				# Exchange Gain/Loss Journal should've been created.
 | |
| 				exc_je_for_si = self.get_journals_for(si.doctype, si.name)
 | |
| 				exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
 | |
| 				self.assertNotEqual(exc_je_for_si, [])
 | |
| 				self.assertEqual(len(exc_je_for_si), 1)
 | |
| 				self.assertEqual(len(exc_je_for_adv), 1)
 | |
| 				self.assertEqual(exc_je_for_si, exc_je_for_adv)
 | |
| 
 | |
| 				# Cancel Invoice
 | |
| 				si.cancel()
 | |
| 
 | |
| 				# Exchange Gain/Loss Journal should've been cancelled
 | |
| 				exc_je_for_si = self.get_journals_for(si.doctype, si.name)
 | |
| 				exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
 | |
| 				self.assertEqual(exc_je_for_si, [])
 | |
| 				self.assertEqual(exc_je_for_adv, [])
 | |
| 
 | |
| 	def test_12_partial_advance_and_payment_for_sales_invoice(self):
 | |
| 		"""
 | |
| 		Sales invoice with partial advance payment, and a normal payment reconciled
 | |
| 		"""
 | |
| 		# Partial Advance
 | |
| 		adv = self.create_payment_entry(amount=1, source_exc_rate=85).save().submit()
 | |
| 		adv.reload()
 | |
| 
 | |
| 		# sales invoice with advance(partial amount)
 | |
| 		rate = 80
 | |
| 		rate_in_account_currency = 1
 | |
| 		si = self.create_sales_invoice(
 | |
| 			qty=2, conversion_rate=80, rate=rate_in_account_currency, do_not_submit=True
 | |
| 		)
 | |
| 		advances = si.get_advance_entries()
 | |
| 		self.assertEqual(len(advances), 1)
 | |
| 		self.assertEqual(advances[0].reference_name, adv.name)
 | |
| 		si.append(
 | |
| 			"advances",
 | |
| 			{
 | |
| 				"doctype": "Sales Invoice Advance",
 | |
| 				"reference_type": advances[0].reference_type,
 | |
| 				"reference_name": advances[0].reference_name,
 | |
| 				"advance_amount": 1,
 | |
| 				"allocated_amount": 1,
 | |
| 				"ref_exchange_rate": advances[0].exchange_rate,
 | |
| 				"remarks": advances[0].remarks,
 | |
| 			},
 | |
| 		)
 | |
| 		si = si.save()
 | |
| 		si = si.submit()
 | |
| 
 | |
| 		# Outstanding should be there in both currencies
 | |
| 		si.reload()
 | |
| 		self.assertEqual(si.outstanding_amount, 1)  # account currency
 | |
| 		self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
 | |
| 
 | |
| 		# Exchange Gain/Loss Journal should've been created for the partial advance
 | |
| 		exc_je_for_si = self.get_journals_for(si.doctype, si.name)
 | |
| 		exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
 | |
| 		self.assertNotEqual(exc_je_for_si, [])
 | |
| 		self.assertEqual(len(exc_je_for_si), 1)
 | |
| 		self.assertEqual(len(exc_je_for_adv), 1)
 | |
| 		self.assertEqual(exc_je_for_si, exc_je_for_adv)
 | |
| 
 | |
| 		# Payment for remaining amount
 | |
| 		pe = self.create_payment_entry(amount=1, source_exc_rate=75).save()
 | |
| 		pe.append(
 | |
| 			"references",
 | |
| 			{"reference_doctype": si.doctype, "reference_name": si.name, "allocated_amount": 1},
 | |
| 		)
 | |
| 		pe = pe.save().submit()
 | |
| 
 | |
| 		# Outstanding in both currencies should be '0'
 | |
| 		si.reload()
 | |
| 		self.assertEqual(si.outstanding_amount, 0)
 | |
| 		self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
 | |
| 
 | |
| 		# Exchange Gain/Loss Journal should've been created for the payment
 | |
| 		exc_je_for_si = self.get_journals_for(si.doctype, si.name)
 | |
| 		exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
 | |
| 		self.assertNotEqual(exc_je_for_si, [])
 | |
| 		# There should be 2 JE's now. One for the advance and one for the payment
 | |
| 		self.assertEqual(len(exc_je_for_si), 2)
 | |
| 		self.assertEqual(len(exc_je_for_pe), 1)
 | |
| 		self.assertEqual(exc_je_for_si, exc_je_for_pe + exc_je_for_adv)
 | |
| 
 | |
| 		# Cancel Invoice
 | |
| 		si.reload()
 | |
| 		si.cancel()
 | |
| 
 | |
| 		# Exchange Gain/Loss Journal should been cancelled
 | |
| 		exc_je_for_si = self.get_journals_for(si.doctype, si.name)
 | |
| 		exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
 | |
| 		exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
 | |
| 		self.assertEqual(exc_je_for_si, [])
 | |
| 		self.assertEqual(exc_je_for_pe, [])
 | |
| 		self.assertEqual(exc_je_for_adv, [])
 | |
| 
 | |
| 	def test_13_partial_advance_and_payment_for_invoice_with_cancellation(self):
 | |
| 		"""
 | |
| 		Invoice with partial advance payment, and a normal payment. Then cancel advance and payment.
 | |
| 		"""
 | |
| 		# Partial Advance
 | |
| 		adv = self.create_payment_entry(amount=1, source_exc_rate=85).save().submit()
 | |
| 		adv.reload()
 | |
| 
 | |
| 		# invoice with advance(partial amount)
 | |
| 		si = self.create_sales_invoice(qty=2, conversion_rate=80, rate=1, do_not_submit=True)
 | |
| 		advances = si.get_advance_entries()
 | |
| 		self.assertEqual(len(advances), 1)
 | |
| 		self.assertEqual(advances[0].reference_name, adv.name)
 | |
| 		si.append(
 | |
| 			"advances",
 | |
| 			{
 | |
| 				"doctype": "Sales Invoice Advance",
 | |
| 				"reference_type": advances[0].reference_type,
 | |
| 				"reference_name": advances[0].reference_name,
 | |
| 				"advance_amount": 1,
 | |
| 				"allocated_amount": 1,
 | |
| 				"ref_exchange_rate": advances[0].exchange_rate,
 | |
| 				"remarks": advances[0].remarks,
 | |
| 			},
 | |
| 		)
 | |
| 		si = si.save()
 | |
| 		si = si.submit()
 | |
| 
 | |
| 		# Outstanding should be there in both currencies
 | |
| 		si.reload()
 | |
| 		self.assertEqual(si.outstanding_amount, 1)  # account currency
 | |
| 		self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
 | |
| 
 | |
| 		# Exchange Gain/Loss Journal should've been created for the partial advance
 | |
| 		exc_je_for_si = self.get_journals_for(si.doctype, si.name)
 | |
| 		exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
 | |
| 		self.assertNotEqual(exc_je_for_si, [])
 | |
| 		self.assertEqual(len(exc_je_for_si), 1)
 | |
| 		self.assertEqual(len(exc_je_for_adv), 1)
 | |
| 		self.assertEqual(exc_je_for_si, exc_je_for_adv)
 | |
| 
 | |
| 		# Payment(remaining amount)
 | |
| 		pe = self.create_payment_entry(amount=1, source_exc_rate=75).save()
 | |
| 		pe.append(
 | |
| 			"references",
 | |
| 			{"reference_doctype": si.doctype, "reference_name": si.name, "allocated_amount": 1},
 | |
| 		)
 | |
| 		pe = pe.save().submit()
 | |
| 
 | |
| 		# Outstanding should be '0' in both currencies
 | |
| 		si.reload()
 | |
| 		self.assertEqual(si.outstanding_amount, 0)
 | |
| 		self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
 | |
| 
 | |
| 		# Exchange Gain/Loss Journal should've been created for the payment
 | |
| 		exc_je_for_si = self.get_journals_for(si.doctype, si.name)
 | |
| 		exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
 | |
| 		self.assertNotEqual(exc_je_for_si, [])
 | |
| 		# There should be 2 JE's now. One for the advance and one for the payment
 | |
| 		self.assertEqual(len(exc_je_for_si), 2)
 | |
| 		self.assertEqual(len(exc_je_for_pe), 1)
 | |
| 		self.assertEqual(exc_je_for_si, exc_je_for_pe + exc_je_for_adv)
 | |
| 
 | |
| 		adv.reload()
 | |
| 		adv.cancel()
 | |
| 
 | |
| 		# Outstanding should be there in both currencies, since advance is cancelled.
 | |
| 		si.reload()
 | |
| 		self.assertEqual(si.outstanding_amount, 1)  # account currency
 | |
| 		self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
 | |
| 
 | |
| 		exc_je_for_si = self.get_journals_for(si.doctype, si.name)
 | |
| 		exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
 | |
| 		exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
 | |
| 		# Exchange Gain/Loss Journal for advance should been cancelled
 | |
| 		self.assertEqual(len(exc_je_for_si), 1)
 | |
| 		self.assertEqual(len(exc_je_for_pe), 1)
 | |
| 		self.assertEqual(exc_je_for_adv, [])
 | |
| 
 | |
| 	def test_14_same_payment_split_against_invoice(self):
 | |
| 		# Invoice in Foreign Currency
 | |
| 		si = self.create_sales_invoice(qty=2, conversion_rate=80, rate=1)
 | |
| 		# Payment
 | |
| 		pe = self.create_payment_entry(amount=2, source_exc_rate=75).save()
 | |
| 		pe.append(
 | |
| 			"references",
 | |
| 			{"reference_doctype": si.doctype, "reference_name": si.name, "allocated_amount": 1},
 | |
| 		)
 | |
| 		pe = pe.save().submit()
 | |
| 
 | |
| 		# There should be outstanding in both currencies
 | |
| 		si.reload()
 | |
| 		self.assertEqual(si.outstanding_amount, 1)
 | |
| 		self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
 | |
| 
 | |
| 		# Exchange Gain/Loss Journal should've been created.
 | |
| 		exc_je_for_si = self.get_journals_for(si.doctype, si.name)
 | |
| 		exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
 | |
| 		self.assertNotEqual(exc_je_for_si, [])
 | |
| 		self.assertEqual(len(exc_je_for_si), 1)
 | |
| 		self.assertEqual(len(exc_je_for_pe), 1)
 | |
| 		self.assertEqual(exc_je_for_si[0], exc_je_for_pe[0])
 | |
| 
 | |
| 		# Reconcile the remaining amount
 | |
| 		pr = frappe.get_doc("Payment Reconciliation")
 | |
| 		pr.company = self.company
 | |
| 		pr.party_type = "Customer"
 | |
| 		pr.party = self.customer
 | |
| 		pr.receivable_payable_account = self.debit_usd
 | |
| 		pr.get_unreconciled_entries()
 | |
| 		self.assertEqual(len(pr.invoices), 1)
 | |
| 		self.assertEqual(len(pr.payments), 1)
 | |
| 		invoices = [x.as_dict() for x in pr.invoices]
 | |
| 		payments = [x.as_dict() for x in pr.payments]
 | |
| 		pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
 | |
| 		pr.reconcile()
 | |
| 		self.assertEqual(len(pr.invoices), 0)
 | |
| 		self.assertEqual(len(pr.payments), 0)
 | |
| 
 | |
| 		# Exc gain/loss journal should have been creaetd for the reconciled amount
 | |
| 		exc_je_for_si = self.get_journals_for(si.doctype, si.name)
 | |
| 		exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
 | |
| 		self.assertEqual(len(exc_je_for_si), 2)
 | |
| 		self.assertEqual(len(exc_je_for_pe), 2)
 | |
| 		self.assertEqual(exc_je_for_si, exc_je_for_pe)
 | |
| 
 | |
| 		# There should be no outstanding
 | |
| 		si.reload()
 | |
| 		self.assertEqual(si.outstanding_amount, 0)
 | |
| 		self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
 | |
| 
 | |
| 		# Cancel Payment
 | |
| 		pe.reload()
 | |
| 		pe.cancel()
 | |
| 
 | |
| 		si.reload()
 | |
| 		self.assertEqual(si.outstanding_amount, 2)
 | |
| 		self.assert_ledger_outstanding(si.doctype, si.name, 160.0, 2.0)
 | |
| 
 | |
| 		# Exchange Gain/Loss Journal should've been cancelled
 | |
| 		exc_je_for_si = self.get_journals_for(si.doctype, si.name)
 | |
| 		exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
 | |
| 		self.assertEqual(exc_je_for_si, [])
 | |
| 		self.assertEqual(exc_je_for_pe, [])
 | |
| 
 | |
| 	def test_20_journal_against_sales_invoice(self):
 | |
| 		# Invoice in Foreign Currency
 | |
| 		si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1)
 | |
| 		# Payment
 | |
| 		je = self.create_journal_entry(
 | |
| 			acc1=self.debit_usd,
 | |
| 			acc1_exc_rate=75,
 | |
| 			acc2=self.cash,
 | |
| 			acc1_amount=-1,
 | |
| 			acc2_amount=-75,
 | |
| 			acc2_exc_rate=1,
 | |
| 		)
 | |
| 		je.accounts[0].party_type = "Customer"
 | |
| 		je.accounts[0].party = self.customer
 | |
| 		je = je.save().submit()
 | |
| 
 | |
| 		# Reconcile the remaining amount
 | |
| 		pr = self.create_payment_reconciliation()
 | |
| 		# pr.receivable_payable_account = self.debit_usd
 | |
| 		pr.get_unreconciled_entries()
 | |
| 		self.assertEqual(len(pr.invoices), 1)
 | |
| 		self.assertEqual(len(pr.payments), 1)
 | |
| 		invoices = [x.as_dict() for x in pr.invoices]
 | |
| 		payments = [x.as_dict() for x in pr.payments]
 | |
| 		pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
 | |
| 		pr.reconcile()
 | |
| 		self.assertEqual(len(pr.invoices), 0)
 | |
| 		self.assertEqual(len(pr.payments), 0)
 | |
| 
 | |
| 		# There should be no outstanding in both currencies
 | |
| 		si.reload()
 | |
| 		self.assertEqual(si.outstanding_amount, 0)
 | |
| 		self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
 | |
| 
 | |
| 		# Exchange Gain/Loss Journal should've been created.
 | |
| 		exc_je_for_si = self.get_journals_for(si.doctype, si.name)
 | |
| 		exc_je_for_je = self.get_journals_for(je.doctype, je.name)
 | |
| 		self.assertNotEqual(exc_je_for_si, [])
 | |
| 		self.assertEqual(
 | |
| 			len(exc_je_for_si), 2
 | |
| 		)  # payment also has reference. so, there are 2 journals referencing invoice
 | |
| 		self.assertEqual(len(exc_je_for_je), 1)
 | |
| 		self.assertIn(exc_je_for_je[0], exc_je_for_si)
 | |
| 
 | |
| 		# Cancel Payment
 | |
| 		je.reload()
 | |
| 		je.cancel()
 | |
| 
 | |
| 		si.reload()
 | |
| 		self.assertEqual(si.outstanding_amount, 1)
 | |
| 		self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
 | |
| 
 | |
| 		# Exchange Gain/Loss Journal should've been cancelled
 | |
| 		exc_je_for_si = self.get_journals_for(si.doctype, si.name)
 | |
| 		exc_je_for_je = self.get_journals_for(je.doctype, je.name)
 | |
| 		self.assertEqual(exc_je_for_si, [])
 | |
| 		self.assertEqual(exc_je_for_je, [])
 | |
| 
 | |
| 	def test_21_advance_journal_against_sales_invoice(self):
 | |
| 		# Advance Payment
 | |
| 		adv_exc_rate = 80
 | |
| 		adv = self.create_journal_entry(
 | |
| 			acc1=self.debit_usd,
 | |
| 			acc1_exc_rate=adv_exc_rate,
 | |
| 			acc2=self.cash,
 | |
| 			acc1_amount=-1,
 | |
| 			acc2_amount=adv_exc_rate * -1,
 | |
| 			acc2_exc_rate=1,
 | |
| 		)
 | |
| 		adv.accounts[0].party_type = "Customer"
 | |
| 		adv.accounts[0].party = self.customer
 | |
| 		adv.accounts[0].is_advance = "Yes"
 | |
| 		adv = adv.save().submit()
 | |
| 		adv.reload()
 | |
| 
 | |
| 		# Sales Invoices in different exchange rates
 | |
| 		for exc_rate in [75.9, 83.1]:
 | |
| 			with self.subTest(exc_rate=exc_rate):
 | |
| 				si = self.create_sales_invoice(qty=1, conversion_rate=exc_rate, rate=1, do_not_submit=True)
 | |
| 				advances = si.get_advance_entries()
 | |
| 				self.assertEqual(len(advances), 1)
 | |
| 				self.assertEqual(advances[0].reference_name, adv.name)
 | |
| 				si.append(
 | |
| 					"advances",
 | |
| 					{
 | |
| 						"doctype": "Sales Invoice Advance",
 | |
| 						"reference_type": advances[0].reference_type,
 | |
| 						"reference_name": advances[0].reference_name,
 | |
| 						"reference_row": advances[0].reference_row,
 | |
| 						"advance_amount": 1,
 | |
| 						"allocated_amount": 1,
 | |
| 						"ref_exchange_rate": advances[0].exchange_rate,
 | |
| 						"remarks": advances[0].remarks,
 | |
| 					},
 | |
| 				)
 | |
| 
 | |
| 				si = si.save()
 | |
| 				si = si.submit()
 | |
| 
 | |
| 				# Outstanding in both currencies should be '0'
 | |
| 				adv.reload()
 | |
| 				self.assertEqual(si.outstanding_amount, 0)
 | |
| 				self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
 | |
| 
 | |
| 				# Exchange Gain/Loss Journal should've been created.
 | |
| 				exc_je_for_si = [x for x in self.get_journals_for(si.doctype, si.name) if x.parent != adv.name]
 | |
| 				exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
 | |
| 				self.assertNotEqual(exc_je_for_si, [])
 | |
| 				self.assertEqual(len(exc_je_for_si), 1)
 | |
| 				self.assertEqual(len(exc_je_for_adv), 1)
 | |
| 				self.assertEqual(exc_je_for_si, exc_je_for_adv)
 | |
| 
 | |
| 				# Cancel Invoice
 | |
| 				si.cancel()
 | |
| 
 | |
| 				# Exchange Gain/Loss Journal should've been cancelled
 | |
| 				exc_je_for_si = self.get_journals_for(si.doctype, si.name)
 | |
| 				exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
 | |
| 				self.assertEqual(exc_je_for_si, [])
 | |
| 				self.assertEqual(exc_je_for_adv, [])
 | |
| 
 | |
| 	def test_22_partial_advance_and_payment_for_invoice_with_cancellation(self):
 | |
| 		"""
 | |
| 		Invoice with partial advance payment as Journal, and a normal payment. Then cancel advance and payment.
 | |
| 		"""
 | |
| 		# Partial Advance
 | |
| 		adv_exc_rate = 75
 | |
| 		adv = self.create_journal_entry(
 | |
| 			acc1=self.debit_usd,
 | |
| 			acc1_exc_rate=adv_exc_rate,
 | |
| 			acc2=self.cash,
 | |
| 			acc1_amount=-1,
 | |
| 			acc2_amount=adv_exc_rate * -1,
 | |
| 			acc2_exc_rate=1,
 | |
| 		)
 | |
| 		adv.accounts[0].party_type = "Customer"
 | |
| 		adv.accounts[0].party = self.customer
 | |
| 		adv.accounts[0].is_advance = "Yes"
 | |
| 		adv = adv.save().submit()
 | |
| 		adv.reload()
 | |
| 
 | |
| 		# invoice with advance(partial amount)
 | |
| 		si = self.create_sales_invoice(qty=3, conversion_rate=80, rate=1, do_not_submit=True)
 | |
| 		advances = si.get_advance_entries()
 | |
| 		self.assertEqual(len(advances), 1)
 | |
| 		self.assertEqual(advances[0].reference_name, adv.name)
 | |
| 		si.append(
 | |
| 			"advances",
 | |
| 			{
 | |
| 				"doctype": "Sales Invoice Advance",
 | |
| 				"reference_type": advances[0].reference_type,
 | |
| 				"reference_name": advances[0].reference_name,
 | |
| 				"reference_row": advances[0].reference_row,
 | |
| 				"advance_amount": 1,
 | |
| 				"allocated_amount": 1,
 | |
| 				"ref_exchange_rate": advances[0].exchange_rate,
 | |
| 				"remarks": advances[0].remarks,
 | |
| 			},
 | |
| 		)
 | |
| 
 | |
| 		si = si.save()
 | |
| 		si = si.submit()
 | |
| 
 | |
| 		# Outstanding should be there in both currencies
 | |
| 		si.reload()
 | |
| 		self.assertEqual(si.outstanding_amount, 2)  # account currency
 | |
| 		self.assert_ledger_outstanding(si.doctype, si.name, 160.0, 2.0)
 | |
| 
 | |
| 		# Exchange Gain/Loss Journal should've been created for the partial advance
 | |
| 		exc_je_for_si = [x for x in self.get_journals_for(si.doctype, si.name) if x.parent != adv.name]
 | |
| 		exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
 | |
| 		self.assertNotEqual(exc_je_for_si, [])
 | |
| 		self.assertEqual(len(exc_je_for_si), 1)
 | |
| 		self.assertEqual(len(exc_je_for_adv), 1)
 | |
| 		self.assertEqual(exc_je_for_si, exc_je_for_adv)
 | |
| 
 | |
| 		# Payment
 | |
| 		adv2_exc_rate = 83
 | |
| 		pay = self.create_journal_entry(
 | |
| 			acc1=self.debit_usd,
 | |
| 			acc1_exc_rate=adv2_exc_rate,
 | |
| 			acc2=self.cash,
 | |
| 			acc1_amount=-2,
 | |
| 			acc2_amount=adv2_exc_rate * -2,
 | |
| 			acc2_exc_rate=1,
 | |
| 		)
 | |
| 		pay.accounts[0].party_type = "Customer"
 | |
| 		pay.accounts[0].party = self.customer
 | |
| 		pay.accounts[0].is_advance = "Yes"
 | |
| 		pay = pay.save().submit()
 | |
| 		pay.reload()
 | |
| 
 | |
| 		# Reconcile the remaining amount
 | |
| 		pr = self.create_payment_reconciliation()
 | |
| 		# pr.receivable_payable_account = self.debit_usd
 | |
| 		pr.get_unreconciled_entries()
 | |
| 		self.assertEqual(len(pr.invoices), 1)
 | |
| 		self.assertEqual(len(pr.payments), 1)
 | |
| 		invoices = [x.as_dict() for x in pr.invoices]
 | |
| 		payments = [x.as_dict() for x in pr.payments]
 | |
| 		pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
 | |
| 		pr.reconcile()
 | |
| 		self.assertEqual(len(pr.invoices), 0)
 | |
| 		self.assertEqual(len(pr.payments), 0)
 | |
| 
 | |
| 		# Outstanding should be '0' in both currencies
 | |
| 		si.reload()
 | |
| 		self.assertEqual(si.outstanding_amount, 0)
 | |
| 		self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
 | |
| 
 | |
| 		# Exchange Gain/Loss Journal should've been created for the payment
 | |
| 		exc_je_for_si = [
 | |
| 			x
 | |
| 			for x in self.get_journals_for(si.doctype, si.name)
 | |
| 			if x.parent != adv.name and x.parent != pay.name
 | |
| 		]
 | |
| 		exc_je_for_pe = self.get_journals_for(pay.doctype, pay.name)
 | |
| 		self.assertNotEqual(exc_je_for_si, [])
 | |
| 		# There should be 2 JE's now. One for the advance and one for the payment
 | |
| 		self.assertEqual(len(exc_je_for_si), 2)
 | |
| 		self.assertEqual(len(exc_je_for_pe), 1)
 | |
| 		self.assertEqual(exc_je_for_si, exc_je_for_pe + exc_je_for_adv)
 | |
| 
 | |
| 		adv.reload()
 | |
| 		adv.cancel()
 | |
| 
 | |
| 		# Outstanding should be there in both currencies, since advance is cancelled.
 | |
| 		si.reload()
 | |
| 		self.assertEqual(si.outstanding_amount, 1)  # account currency
 | |
| 		self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
 | |
| 
 | |
| 		exc_je_for_si = [
 | |
| 			x
 | |
| 			for x in self.get_journals_for(si.doctype, si.name)
 | |
| 			if x.parent != adv.name and x.parent != pay.name
 | |
| 		]
 | |
| 		exc_je_for_pe = self.get_journals_for(pay.doctype, pay.name)
 | |
| 		exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name)
 | |
| 		# Exchange Gain/Loss Journal for advance should been cancelled
 | |
| 		self.assertEqual(len(exc_je_for_si), 1)
 | |
| 		self.assertEqual(len(exc_je_for_pe), 1)
 | |
| 		self.assertEqual(exc_je_for_adv, [])
 | |
| 
 | |
| 	def test_23_same_journal_split_against_single_invoice(self):
 | |
| 		# Invoice in Foreign Currency
 | |
| 		si = self.create_sales_invoice(qty=2, conversion_rate=80, rate=1)
 | |
| 		# Payment
 | |
| 		je = self.create_journal_entry(
 | |
| 			acc1=self.debit_usd,
 | |
| 			acc1_exc_rate=75,
 | |
| 			acc2=self.cash,
 | |
| 			acc1_amount=-2,
 | |
| 			acc2_amount=-150,
 | |
| 			acc2_exc_rate=1,
 | |
| 		)
 | |
| 		je.accounts[0].party_type = "Customer"
 | |
| 		je.accounts[0].party = self.customer
 | |
| 		je = je.save().submit()
 | |
| 
 | |
| 		# Reconcile the first half
 | |
| 		pr = self.create_payment_reconciliation()
 | |
| 		pr.get_unreconciled_entries()
 | |
| 		self.assertEqual(len(pr.invoices), 1)
 | |
| 		self.assertEqual(len(pr.payments), 1)
 | |
| 		invoices = [x.as_dict() for x in pr.invoices]
 | |
| 		payments = [x.as_dict() for x in pr.payments]
 | |
| 		pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
 | |
| 		difference_amount = pr.calculate_difference_on_allocation_change(
 | |
| 			[x.as_dict() for x in pr.payments], [x.as_dict() for x in pr.invoices], 1
 | |
| 		)
 | |
| 		pr.allocation[0].allocated_amount = 1
 | |
| 		pr.allocation[0].difference_amount = difference_amount
 | |
| 		pr.reconcile()
 | |
| 		self.assertEqual(len(pr.invoices), 1)
 | |
| 		self.assertEqual(len(pr.payments), 1)
 | |
| 
 | |
| 		# There should be outstanding in both currencies
 | |
| 		si.reload()
 | |
| 		self.assertEqual(si.outstanding_amount, 1)
 | |
| 		self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
 | |
| 
 | |
| 		# Exchange Gain/Loss Journal should've been created.
 | |
| 		exc_je_for_si = [x for x in self.get_journals_for(si.doctype, si.name) if x.parent != je.name]
 | |
| 		exc_je_for_je = self.get_journals_for(je.doctype, je.name)
 | |
| 		self.assertNotEqual(exc_je_for_si, [])
 | |
| 		self.assertEqual(len(exc_je_for_si), 1)
 | |
| 		self.assertEqual(len(exc_je_for_je), 1)
 | |
| 		self.assertIn(exc_je_for_je[0], exc_je_for_si)
 | |
| 
 | |
| 		# reconcile remaining half
 | |
| 		pr.get_unreconciled_entries()
 | |
| 		self.assertEqual(len(pr.invoices), 1)
 | |
| 		self.assertEqual(len(pr.payments), 1)
 | |
| 		invoices = [x.as_dict() for x in pr.invoices]
 | |
| 		payments = [x.as_dict() for x in pr.payments]
 | |
| 		pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
 | |
| 		pr.allocation[0].allocated_amount = 1
 | |
| 		pr.allocation[0].difference_amount = difference_amount
 | |
| 		pr.reconcile()
 | |
| 		self.assertEqual(len(pr.invoices), 0)
 | |
| 		self.assertEqual(len(pr.payments), 0)
 | |
| 
 | |
| 		# Exchange Gain/Loss Journal should've been created.
 | |
| 		exc_je_for_si = [x for x in self.get_journals_for(si.doctype, si.name) if x.parent != je.name]
 | |
| 		exc_je_for_je = self.get_journals_for(je.doctype, je.name)
 | |
| 		self.assertNotEqual(exc_je_for_si, [])
 | |
| 		self.assertEqual(len(exc_je_for_si), 2)
 | |
| 		self.assertEqual(len(exc_je_for_je), 2)
 | |
| 		self.assertIn(exc_je_for_je[0], exc_je_for_si)
 | |
| 
 | |
| 		si.reload()
 | |
| 		self.assertEqual(si.outstanding_amount, 0)
 | |
| 		self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
 | |
| 
 | |
| 		# Cancel Payment
 | |
| 		je.reload()
 | |
| 		je.cancel()
 | |
| 
 | |
| 		si.reload()
 | |
| 		self.assertEqual(si.outstanding_amount, 2)
 | |
| 		self.assert_ledger_outstanding(si.doctype, si.name, 160.0, 2.0)
 | |
| 
 | |
| 		# Exchange Gain/Loss Journal should've been cancelled
 | |
| 		exc_je_for_si = self.get_journals_for(si.doctype, si.name)
 | |
| 		exc_je_for_je = self.get_journals_for(je.doctype, je.name)
 | |
| 		self.assertEqual(exc_je_for_si, [])
 | |
| 		self.assertEqual(exc_je_for_je, [])
 | |
| 
 | |
| 	def test_24_journal_against_multiple_invoices(self):
 | |
| 		si1 = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1)
 | |
| 		si2 = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1)
 | |
| 
 | |
| 		# Payment
 | |
| 		je = self.create_journal_entry(
 | |
| 			acc1=self.debit_usd,
 | |
| 			acc1_exc_rate=75,
 | |
| 			acc2=self.cash,
 | |
| 			acc1_amount=-2,
 | |
| 			acc2_amount=-150,
 | |
| 			acc2_exc_rate=1,
 | |
| 		)
 | |
| 		je.accounts[0].party_type = "Customer"
 | |
| 		je.accounts[0].party = self.customer
 | |
| 		je = je.save().submit()
 | |
| 
 | |
| 		pr = self.create_payment_reconciliation()
 | |
| 		pr.get_unreconciled_entries()
 | |
| 		self.assertEqual(len(pr.invoices), 2)
 | |
| 		self.assertEqual(len(pr.payments), 1)
 | |
| 		invoices = [x.as_dict() for x in pr.invoices]
 | |
| 		payments = [x.as_dict() for x in pr.payments]
 | |
| 		pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
 | |
| 		pr.reconcile()
 | |
| 		self.assertEqual(len(pr.invoices), 0)
 | |
| 		self.assertEqual(len(pr.payments), 0)
 | |
| 
 | |
| 		si1.reload()
 | |
| 		si2.reload()
 | |
| 
 | |
| 		self.assertEqual(si1.outstanding_amount, 0)
 | |
| 		self.assertEqual(si2.outstanding_amount, 0)
 | |
| 		self.assert_ledger_outstanding(si1.doctype, si1.name, 0.0, 0.0)
 | |
| 		self.assert_ledger_outstanding(si2.doctype, si2.name, 0.0, 0.0)
 | |
| 
 | |
| 		# Exchange Gain/Loss Journal should've been created
 | |
| 		# remove payment JE from list
 | |
| 		exc_je_for_si1 = [x for x in self.get_journals_for(si1.doctype, si1.name) if x.parent != je.name]
 | |
| 		exc_je_for_si2 = [x for x in self.get_journals_for(si2.doctype, si2.name) if x.parent != je.name]
 | |
| 		exc_je_for_je = [x for x in self.get_journals_for(je.doctype, je.name) if x.parent != je.name]
 | |
| 		self.assertEqual(len(exc_je_for_si1), 1)
 | |
| 		self.assertEqual(len(exc_je_for_si2), 1)
 | |
| 		self.assertEqual(len(exc_je_for_je), 2)
 | |
| 
 | |
| 		si1.cancel()
 | |
| 		# Gain/Loss JE of si1 should've been cancelled
 | |
| 		exc_je_for_si1 = [x for x in self.get_journals_for(si1.doctype, si1.name) if x.parent != je.name]
 | |
| 		exc_je_for_si2 = [x for x in self.get_journals_for(si2.doctype, si2.name) if x.parent != je.name]
 | |
| 		exc_je_for_je = [x for x in self.get_journals_for(je.doctype, je.name) if x.parent != je.name]
 | |
| 		self.assertEqual(len(exc_je_for_si1), 0)
 | |
| 		self.assertEqual(len(exc_je_for_si2), 1)
 | |
| 		self.assertEqual(len(exc_je_for_je), 1)
 | |
| 
 | |
| 	def test_30_cr_note_against_sales_invoice(self):
 | |
| 		"""
 | |
| 		Reconciling Cr Note against Sales Invoice, both having different exchange rates
 | |
| 		"""
 | |
| 		# Invoice in Foreign currency
 | |
| 		si = self.create_sales_invoice(qty=2, conversion_rate=80, rate=1)
 | |
| 
 | |
| 		# Cr Note in Foreign currency of different exchange rate
 | |
| 		cr_note = self.create_sales_invoice(qty=-2, conversion_rate=75, rate=1, do_not_save=True)
 | |
| 		cr_note.is_return = 1
 | |
| 		cr_note.save().submit()
 | |
| 
 | |
| 		# Reconcile the first half
 | |
| 		pr = self.create_payment_reconciliation()
 | |
| 		pr.get_unreconciled_entries()
 | |
| 		self.assertEqual(len(pr.invoices), 1)
 | |
| 		self.assertEqual(len(pr.payments), 1)
 | |
| 		invoices = [x.as_dict() for x in pr.invoices]
 | |
| 		payments = [x.as_dict() for x in pr.payments]
 | |
| 		pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
 | |
| 		difference_amount = pr.calculate_difference_on_allocation_change(
 | |
| 			[x.as_dict() for x in pr.payments], [x.as_dict() for x in pr.invoices], 1
 | |
| 		)
 | |
| 		pr.allocation[0].allocated_amount = 1
 | |
| 		pr.allocation[0].difference_amount = difference_amount
 | |
| 		pr.reconcile()
 | |
| 		self.assertEqual(len(pr.invoices), 1)
 | |
| 		self.assertEqual(len(pr.payments), 1)
 | |
| 
 | |
| 		# Exchange Gain/Loss Journal should've been created.
 | |
| 		exc_je_for_si = self.get_journals_for(si.doctype, si.name)
 | |
| 		exc_je_for_cr = self.get_journals_for(cr_note.doctype, cr_note.name)
 | |
| 		self.assertNotEqual(exc_je_for_si, [])
 | |
| 		self.assertEqual(len(exc_je_for_si), 2)
 | |
| 		self.assertEqual(len(exc_je_for_cr), 2)
 | |
| 		self.assertEqual(exc_je_for_cr, exc_je_for_si)
 | |
| 
 | |
| 		si.reload()
 | |
| 		self.assertEqual(si.outstanding_amount, 1)
 | |
| 		self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
 | |
| 
 | |
| 		cr_note.reload()
 | |
| 		cr_note.cancel()
 | |
| 
 | |
| 		# Exchange Gain/Loss Journal should've been created.
 | |
| 		exc_je_for_si = self.get_journals_for(si.doctype, si.name)
 | |
| 		exc_je_for_cr = self.get_journals_for(cr_note.doctype, cr_note.name)
 | |
| 		self.assertNotEqual(exc_je_for_si, [])
 | |
| 		self.assertEqual(len(exc_je_for_si), 1)
 | |
| 		self.assertEqual(len(exc_je_for_cr), 0)
 | |
| 
 | |
| 		# The Credit Note JE is still active and is referencing the sales invoice
 | |
| 		# So, outstanding stays the same
 | |
| 		si.reload()
 | |
| 		self.assertEqual(si.outstanding_amount, 1)
 | |
| 		self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0)
 | |
| 
 | |
| 	def test_40_cost_center_from_payment_entry(self):
 | |
| 		"""
 | |
| 		Gain/Loss JE should inherit cost center from payment if company default is unset
 | |
| 		"""
 | |
| 		# remove default cost center
 | |
| 		cc = frappe.db.get_value("Company", self.company, "cost_center")
 | |
| 		frappe.db.set_value("Company", self.company, "cost_center", None)
 | |
| 
 | |
| 		rate_in_account_currency = 1
 | |
| 		si = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_submit=True)
 | |
| 		si.cost_center = None
 | |
| 		si.save().submit()
 | |
| 
 | |
| 		pe = get_payment_entry(si.doctype, si.name)
 | |
| 		pe.source_exchange_rate = 75
 | |
| 		pe.received_amount = 75
 | |
| 		pe.cost_center = self.cost_center
 | |
| 		pe = pe.save().submit()
 | |
| 
 | |
| 		# Exchange Gain/Loss Journal should've been created.
 | |
| 		exc_je_for_si = self.get_journals_for(si.doctype, si.name)
 | |
| 		exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
 | |
| 		self.assertNotEqual(exc_je_for_si, [])
 | |
| 		self.assertEqual(len(exc_je_for_si), 1)
 | |
| 		self.assertEqual(len(exc_je_for_pe), 1)
 | |
| 		self.assertEqual(exc_je_for_si[0], exc_je_for_pe[0])
 | |
| 
 | |
| 		self.assertEqual(
 | |
| 			[self.cost_center, self.cost_center],
 | |
| 			frappe.db.get_all(
 | |
| 				"Journal Entry Account", filters={"parent": exc_je_for_si[0].parent}, pluck="cost_center"
 | |
| 			),
 | |
| 		)
 | |
| 		frappe.db.set_value("Company", self.company, "cost_center", cc)
 | |
| 
 | |
| 	def test_41_cost_center_from_journal_entry(self):
 | |
| 		"""
 | |
| 		Gain/Loss JE should inherit cost center from payment if company default is unset
 | |
| 		"""
 | |
| 		# remove default cost center
 | |
| 		cc = frappe.db.get_value("Company", self.company, "cost_center")
 | |
| 		frappe.db.set_value("Company", self.company, "cost_center", None)
 | |
| 
 | |
| 		rate_in_account_currency = 1
 | |
| 		si = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_submit=True)
 | |
| 		si.cost_center = None
 | |
| 		si.save().submit()
 | |
| 
 | |
| 		je = self.create_journal_entry(
 | |
| 			acc1=self.debit_usd,
 | |
| 			acc1_exc_rate=75,
 | |
| 			acc2=self.cash,
 | |
| 			acc1_amount=-1,
 | |
| 			acc2_amount=-75,
 | |
| 			acc2_exc_rate=1,
 | |
| 		)
 | |
| 		je.accounts[0].party_type = "Customer"
 | |
| 		je.accounts[0].party = self.customer
 | |
| 		je.accounts[0].cost_center = self.cost_center
 | |
| 		je = je.save().submit()
 | |
| 
 | |
| 		# Reconcile
 | |
| 		pr = self.create_payment_reconciliation()
 | |
| 		pr.get_unreconciled_entries()
 | |
| 		self.assertEqual(len(pr.invoices), 1)
 | |
| 		self.assertEqual(len(pr.payments), 1)
 | |
| 		invoices = [x.as_dict() for x in pr.invoices]
 | |
| 		payments = [x.as_dict() for x in pr.payments]
 | |
| 		pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
 | |
| 		pr.reconcile()
 | |
| 		self.assertEqual(len(pr.invoices), 0)
 | |
| 		self.assertEqual(len(pr.payments), 0)
 | |
| 
 | |
| 		# Exchange Gain/Loss Journal should've been created.
 | |
| 		exc_je_for_si = [x for x in self.get_journals_for(si.doctype, si.name) if x.parent != je.name]
 | |
| 		exc_je_for_je = [x for x in self.get_journals_for(je.doctype, je.name) if x.parent != je.name]
 | |
| 		self.assertNotEqual(exc_je_for_si, [])
 | |
| 		self.assertEqual(len(exc_je_for_si), 1)
 | |
| 		self.assertEqual(len(exc_je_for_je), 1)
 | |
| 		self.assertEqual(exc_je_for_si[0], exc_je_for_je[0])
 | |
| 
 | |
| 		self.assertEqual(
 | |
| 			[self.cost_center, self.cost_center],
 | |
| 			frappe.db.get_all(
 | |
| 				"Journal Entry Account", filters={"parent": exc_je_for_si[0].parent}, pluck="cost_center"
 | |
| 			),
 | |
| 		)
 | |
| 		frappe.db.set_value("Company", self.company, "cost_center", cc)
 | |
| 
 | |
| 	def test_42_cost_center_from_cr_note(self):
 | |
| 		"""
 | |
| 		Gain/Loss JE should inherit cost center from payment if company default is unset
 | |
| 		"""
 | |
| 		# remove default cost center
 | |
| 		cc = frappe.db.get_value("Company", self.company, "cost_center")
 | |
| 		frappe.db.set_value("Company", self.company, "cost_center", None)
 | |
| 
 | |
| 		rate_in_account_currency = 1
 | |
| 		si = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_submit=True)
 | |
| 		si.cost_center = None
 | |
| 		si.save().submit()
 | |
| 
 | |
| 		cr_note = self.create_sales_invoice(qty=-1, conversion_rate=75, rate=1, do_not_save=True)
 | |
| 		cr_note.cost_center = self.cost_center
 | |
| 		cr_note.is_return = 1
 | |
| 		cr_note.save().submit()
 | |
| 
 | |
| 		# Reconcile
 | |
| 		pr = self.create_payment_reconciliation()
 | |
| 		pr.get_unreconciled_entries()
 | |
| 		self.assertEqual(len(pr.invoices), 1)
 | |
| 		self.assertEqual(len(pr.payments), 1)
 | |
| 		invoices = [x.as_dict() for x in pr.invoices]
 | |
| 		payments = [x.as_dict() for x in pr.payments]
 | |
| 		pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
 | |
| 		pr.reconcile()
 | |
| 		self.assertEqual(len(pr.invoices), 0)
 | |
| 		self.assertEqual(len(pr.payments), 0)
 | |
| 
 | |
| 		# Exchange Gain/Loss Journal should've been created.
 | |
| 		exc_je_for_si = self.get_journals_for(si.doctype, si.name)
 | |
| 		exc_je_for_cr_note = self.get_journals_for(cr_note.doctype, cr_note.name)
 | |
| 		self.assertNotEqual(exc_je_for_si, [])
 | |
| 		self.assertEqual(len(exc_je_for_si), 2)
 | |
| 		self.assertEqual(len(exc_je_for_cr_note), 2)
 | |
| 		self.assertEqual(exc_je_for_si, exc_je_for_cr_note)
 | |
| 
 | |
| 		for x in exc_je_for_si + exc_je_for_cr_note:
 | |
| 			with self.subTest(x=x):
 | |
| 				self.assertEqual(
 | |
| 					[self.cost_center, self.cost_center],
 | |
| 					frappe.db.get_all("Journal Entry Account", filters={"parent": x.parent}, pluck="cost_center"),
 | |
| 				)
 | |
| 
 | |
| 		frappe.db.set_value("Company", self.company, "cost_center", cc)
 |