diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index fe8342d78bd..d2398d21f4e 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -4284,6 +4284,35 @@ class TestSalesInvoice(FrappeTestCase):
pos_return = make_sales_return(pos.name)
self.assertEqual(abs(pos_return.payments[0].amount), pos.payments[0].amount)
+ def test_create_return_invoice_for_self_update(self):
+ 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.controllers.sales_and_purchase_return import make_return_doc
+
+ invoice = create_sales_invoice()
+
+ payment_entry = get_payment_entry(dt=invoice.doctype, dn=invoice.name)
+ payment_entry.reference_no = "test001"
+ payment_entry.reference_date = getdate()
+
+ payment_entry.save()
+ payment_entry.submit()
+
+ r_invoice = make_return_doc(invoice.doctype, invoice.name)
+
+ r_invoice.update_outstanding_for_self = 0
+ r_invoice.save()
+
+ self.assertEqual(r_invoice.update_outstanding_for_self, 1)
+
+ r_invoice.submit()
+
+ self.assertNotEqual(r_invoice.outstanding_amount, 0)
+
+ invoice.reload()
+
+ self.assertEqual(invoice.outstanding_amount, 0)
+
def test_prevents_fully_returned_invoice_with_zero_quantity(self):
from erpnext.controllers.sales_and_purchase_return import StockOverReturnError, make_return_doc
diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
index 39ca78153c3..44fee120d8b 100644
--- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
@@ -21,7 +21,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
def tearDown(self):
frappe.db.rollback()
- def create_sales_invoice(self, no_payment_schedule=False, do_not_submit=False):
+ 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,
@@ -34,6 +34,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
rate=100,
price_list_rate=100,
do_not_save=1,
+ **args,
)
if not no_payment_schedule:
si.append(
@@ -108,7 +109,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
self.assertEqual(expected_data[0], [row.invoiced, row.paid, row.credit_note])
pos_inv.cancel()
- def test_accounts_receivable(self):
+ def test_accounts_receivable_with_payment(self):
filters = {
"company": self.company,
"based_on_payment_terms": 1,
@@ -145,11 +146,15 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
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 = [100, 0, 0, 40, -40, self.debit_to]
+ expected_data_after_credit_note = [0, 0, 100, 0, -100, self.debit_to]
- row = report[1][0]
+ row = report[1][-1]
self.assertEqual(
expected_data_after_credit_note,
[
@@ -162,6 +167,99 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
],
)
+ 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, "No Remarks"], [100, 50, "No Remarks"], [100, 20, "No Remarks"]]
+
+ for i in range(3):
+ row = report[1][i - 1]
+ self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced, row.remarks])
+
+ # 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)
+
+ 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, "No Remarks"], [200, 100, "No Remarks"], [200, 40, "No Remarks"]]
+
+ for i in range(3):
+ row = report[1][i - 1]
+ self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced, row.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,
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index bcaf8f897f9..46418ca8a63 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -165,6 +165,48 @@ class AccountsController(TransactionBase):
raise_exception=1,
)
+ def validate_against_voucher_outstanding(self):
+ from frappe.model.meta import get_meta
+
+ if not get_meta(self.doctype).has_field("outstanding_amount"):
+ return
+
+ if self.get("is_return") and self.return_against and not self.get("is_pos"):
+ against_voucher_outstanding = frappe.get_value(
+ self.doctype, self.return_against, "outstanding_amount"
+ )
+ document_type = "Credit Note" if self.doctype == "Sales Invoice" else "Debit Note"
+
+ msg = ""
+ if self.get("update_outstanding_for_self"):
+ msg = (
+ "We can see {0} is made against {1}. If you want {1}'s outstanding to be updated, "
+ "uncheck '{2}' checkbox.
Or"
+ ).format(
+ frappe.bold(document_type),
+ get_link_to_form(self.doctype, self.get("return_against")),
+ frappe.bold(_("Update Outstanding for Self")),
+ )
+
+ elif not self.update_outstanding_for_self and (
+ abs(flt(self.rounded_total) or flt(self.grand_total)) > flt(against_voucher_outstanding)
+ ):
+ self.update_outstanding_for_self = 1
+ msg = (
+ "The outstanding amount {} in {} is lesser than {}. Updating the outstanding to this invoice.
And"
+ ).format(
+ against_voucher_outstanding,
+ get_link_to_form(self.doctype, self.get("return_against")),
+ flt(abs(self.outstanding_amount)),
+ )
+
+ if msg:
+ msg += " you can use {} tool to reconcile against {} later.".format(
+ get_link_to_form("Payment Reconciliation", "Payment Reconciliation"),
+ get_link_to_form(self.doctype, self.get("return_against")),
+ )
+ frappe.msgprint(_(msg))
+
def validate(self):
if not self.get("is_return") and not self.get("is_debit_note"):
self.validate_qty_is_not_zero()
@@ -193,6 +235,7 @@ class AccountsController(TransactionBase):
self.disable_tax_included_prices_for_internal_transfer()
self.set_incoming_rate()
self.init_internal_values()
+ self.validate_against_voucher_outstanding()
# Need to set taxes based on taxes_and_charges template
# before calculating taxes and totals
@@ -228,20 +271,6 @@ class AccountsController(TransactionBase):
)
)
- if self.get("is_return") and self.get("return_against") and not self.get("is_pos"):
- if self.get("update_outstanding_for_self"):
- document_type = "Credit Note" if self.doctype == "Sales Invoice" else "Debit Note"
- frappe.msgprint(
- _(
- "We can see {0} is made against {1}. If you want {1}'s outstanding to be updated, uncheck '{2}' checkbox.
Or you can use {3} tool to reconcile against {1} later."
- ).format(
- frappe.bold(document_type),
- get_link_to_form(self.doctype, self.get("return_against")),
- frappe.bold(_("Update Outstanding for Self")),
- get_link_to_form("Payment Reconciliation", "Payment Reconciliation"),
- )
- )
-
pos_check_field = "is_pos" if self.doctype == "Sales Invoice" else "is_paid"
if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)):
self.set_advances()
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index 091b08cad0f..955c9261031 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -819,9 +819,12 @@ class calculate_taxes_and_totals:
if (
self.doc.is_return
and self.doc.return_against
+ and not self.doc.update_outstanding_for_self
and not self.doc.get("is_pos")
or self.is_internal_invoice()
):
+ # Do not calculate the outstanding amount for a return invoice if 'update_outstanding_for_self' is not enabled.
+ self.doc.outstanding_amount = 0
return
self.doc.round_floats_in(self.doc, ["grand_total", "total_advance", "write_off_amount"])