diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index ae2625b6539..785b8a180b1 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -1061,6 +1061,101 @@ class TestPaymentEntry(FrappeTestCase): } self.assertDictEqual(ref_details, expected_response) + @change_settings( + "Accounts Settings", + { + "unlink_payment_on_cancellation_of_invoice": 1, + "delete_linked_ledger_entries": 1, + "allow_multi_currency_invoices_against_single_party_account": 1, + }, + ) + def test_overallocation_validation_on_payment_terms(self): + """ + Validate Allocation on Payment Entry based on Payment Schedule. Upon overallocation, validation error must be thrown. + + """ + customer = create_customer() + create_payment_terms_template() + + # Validate allocation on base/company currency + si1 = create_sales_invoice(do_not_save=1, qty=1, rate=200) + si1.payment_terms_template = "Test Receivable Template" + si1.save().submit() + + si1.reload() + pe = get_payment_entry(si1.doctype, si1.name).save() + # Allocated amount should be according to the payment schedule + for idx, schedule in enumerate(si1.payment_schedule): + with self.subTest(idx=idx): + self.assertEqual(flt(schedule.payment_amount), flt(pe.references[idx].allocated_amount)) + pe.save() + + # Overallocation validation should trigger + pe.paid_amount = 400 + pe.references[0].allocated_amount = 200 + pe.references[1].allocated_amount = 200 + self.assertRaises(frappe.ValidationError, pe.save) + pe.delete() + si1.cancel() + si1.delete() + + # Validate allocation on foreign currency + si2 = create_sales_invoice( + customer="_Test Customer USD", + debit_to="_Test Receivable USD - _TC", + currency="USD", + conversion_rate=80, + do_not_save=1, + ) + si2.payment_terms_template = "Test Receivable Template" + si2.save().submit() + + si2.reload() + pe = get_payment_entry(si2.doctype, si2.name).save() + # Allocated amount should be according to the payment schedule + for idx, schedule in enumerate(si2.payment_schedule): + with self.subTest(idx=idx): + self.assertEqual(flt(schedule.payment_amount), flt(pe.references[idx].allocated_amount)) + pe.save() + + # Overallocation validation should trigger + pe.paid_amount = 200 + pe.references[0].allocated_amount = 100 + pe.references[1].allocated_amount = 100 + self.assertRaises(frappe.ValidationError, pe.save) + pe.delete() + si2.cancel() + si2.delete() + + # Validate allocation in base/company currency on a foreign currency document + # when invoice is made is foreign currency, but posted to base/company currency debtors account + si3 = create_sales_invoice( + customer=customer, + currency="USD", + conversion_rate=80, + do_not_save=1, + ) + si3.payment_terms_template = "Test Receivable Template" + si3.save().submit() + + si3.reload() + pe = get_payment_entry(si3.doctype, si3.name).save() + # Allocated amount should be equal to payment term outstanding + self.assertEqual(len(pe.references), 2) + for idx, ref in enumerate(pe.references): + with self.subTest(idx=idx): + self.assertEqual(ref.payment_term_outstanding, ref.allocated_amount) + pe.save() + + # Overallocation validation should trigger + pe.paid_amount = 16000 + pe.references[0].allocated_amount = 8000 + pe.references[1].allocated_amount = 8000 + self.assertRaises(frappe.ValidationError, pe.save) + pe.delete() + si3.cancel() + si3.delete() + def create_payment_entry(**args): payment_entry = frappe.new_doc("Payment Entry") @@ -1150,3 +1245,17 @@ def create_payment_terms_template_with_discount( def create_payment_term(name): if not frappe.db.exists("Payment Term", name): frappe.get_doc({"doctype": "Payment Term", "payment_term_name": name}).insert() + + +def create_customer(name="_Test Customer 2 USD", currency="USD"): + customer = None + if frappe.db.exists("Customer", name): + customer = name + else: + customer = frappe.new_doc("Customer") + customer.customer_name = name + customer.default_currency = currency + customer.type = "Individual" + customer.save() + customer = customer.name + return customer