From fb06f886d261c2b674ff3851a5b0bcdde1bdfb0f Mon Sep 17 00:00:00 2001 From: Bhavan23 Date: Thu, 20 Mar 2025 19:19:47 +0530 Subject: [PATCH 1/4] fix(accounting): update outstanding amount based on update_outstanding_for_self fix(accounting): against voucher has been already paid show proper message and update update_outstanding_for_self as 1 (cherry picked from commit 222f1834f1c4264bdea2a64ecc69a87ef7124106) # Conflicts: # erpnext/controllers/accounts_controller.py --- erpnext/controllers/accounts_controller.py | 46 ++++++++++++++++++++++ erpnext/controllers/taxes_and_totals.py | 3 ++ 2 files changed, 49 insertions(+) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index bcaf8f897f9..d8581f58a4f 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"), + 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,6 +271,7 @@ class AccountsController(TransactionBase): ) ) +<<<<<<< HEAD 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" @@ -242,6 +286,8 @@ class AccountsController(TransactionBase): ) ) +======= +>>>>>>> 222f1834f1 (fix(accounting): update outstanding amount based on update_outstanding_for_self) 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"]) From 88e11d6fd94f6aa0e224b29f50b63d176d689c97 Mon Sep 17 00:00:00 2001 From: Bhavan23 Date: Thu, 20 Mar 2025 19:20:41 +0530 Subject: [PATCH 2/4] test: add unit test to validate outstanding amount for update_outstanding_for_self checkbox enabled (cherry picked from commit 7b0882600a29ddd641115c062e961cedc84477dd) --- .../sales_invoice/test_sales_invoice.py | 29 +++++ .../test_accounts_receivable.py | 106 +++++++++++++++++- 2 files changed, 131 insertions(+), 4 deletions(-) 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, From d6b488f52962a30f0374d9997c242c41b0b8d721 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 8 Apr 2025 14:36:15 +0530 Subject: [PATCH 3/4] chore: resolve conflict --- erpnext/controllers/accounts_controller.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index d8581f58a4f..31d157bed08 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -271,23 +271,6 @@ class AccountsController(TransactionBase): ) ) -<<<<<<< HEAD - 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"), - ) - ) - -======= ->>>>>>> 222f1834f1 (fix(accounting): update outstanding amount based on update_outstanding_for_self) 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() From 4eb0f39af720b845959b551d70f2129e5ac7a40d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 8 Apr 2025 15:15:16 +0530 Subject: [PATCH 4/4] chore: pass doctype and name --- erpnext/controllers/accounts_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 31d157bed08..46418ca8a63 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -202,7 +202,7 @@ class AccountsController(TransactionBase): if msg: msg += " you can use {} tool to reconcile against {} later.".format( - get_link_to_form("Payment Reconciliation"), + get_link_to_form("Payment Reconciliation", "Payment Reconciliation"), get_link_to_form(self.doctype, self.get("return_against")), ) frappe.msgprint(_(msg))