From 8e2bfc6bcbb65be8b3ee9e220ed49ae962f57e01 Mon Sep 17 00:00:00 2001 From: Bhavan23 Date: Thu, 20 Mar 2025 19:19:47 +0530 Subject: [PATCH 1/5] 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 0ba33999c5c..a146628cd25 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -153,6 +153,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() @@ -178,6 +220,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() if self.meta.get_field("currency"): self.calculate_taxes_and_totals() @@ -209,6 +252,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" @@ -223,6 +267,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 313ed5c0415..ced62b71cf9 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -795,9 +795,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 c7e6b2356f28f087b0f20f8dc15acd2d981fbe27 Mon Sep 17 00:00:00 2001 From: Bhavan23 Date: Thu, 20 Mar 2025 19:20:41 +0530 Subject: [PATCH 2/5] test: add unit test to validate outstanding amount for update_outstanding_for_self checkbox enabled (cherry picked from commit 7b0882600a29ddd641115c062e961cedc84477dd) # Conflicts: # erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py --- .../sales_invoice/test_sales_invoice.py | 100 +++++++++++++++++ .../test_accounts_receivable.py | 106 +++++++++++++++++- 2 files changed, 202 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 3c1bc848f74..9917596f8be 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -3861,6 +3861,7 @@ class TestSalesInvoice(FrappeTestCase): self.assertEqual(doc.total_billed_amount, si.grand_total) +<<<<<<< HEAD def check_gl_entries(doc, voucher_no, expected_gle, posting_date): gl_entries = frappe.db.sql( """select account, debit, credit, posting_date @@ -3870,6 +3871,105 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date): order by posting_date asc, account asc""", (voucher_no, posting_date), as_dict=1, +======= + si = create_sales_invoice(do_not_submit=True) + + project = frappe.new_doc("Project") + project.company = "_Test Company" + project.project_name = "Test Total Billed Amount" + project.save() + + si.project = project.name + si.items.append(copy(si.items[0])) + si.items.append(copy(si.items[0])) + si.items[0].project = project.name + si.items[1].project = project.name + # Not setting project on last item + si.items[1].insert() + si.items[2].insert() + si.submit() + + project.reload() + self.assertIsNone(si.items[2].project) + self.assertEqual(project.total_billed_amount, 300) + + def test_pos_returns_with_party_account_currency(self): + from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return + + pos_profile = make_pos_profile() + pos_profile.payments = [] + pos_profile.append("payments", {"default": 1, "mode_of_payment": "Cash"}) + pos_profile.save() + + pos = create_sales_invoice( + customer="_Test Customer USD", + currency="USD", + conversion_rate=86.595000000, + qty=2, + do_not_save=True, + ) + pos.is_pos = 1 + pos.pos_profile = pos_profile.name + pos.debit_to = "_Test Receivable USD - _TC" + pos.append("payments", {"mode_of_payment": "Cash", "account": "_Test Bank - _TC", "amount": 20.35}) + pos.save().submit() + + 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 + + invoice = create_sales_invoice(qty=10) + + return_doc = make_return_doc(invoice.doctype, invoice.name) + return_doc.items[0].qty = -10 + return_doc.save().submit() + + return_doc = make_return_doc(invoice.doctype, invoice.name) + return_doc.items[0].qty = 0 + + self.assertRaises(StockOverReturnError, return_doc.save) + + +def set_advance_flag(company, flag, default_account): + frappe.db.set_value( + "Company", + company, + { + "book_advance_payments_in_separate_party_account": flag, + "default_advance_received_account": default_account, + }, +>>>>>>> 7b0882600a (test: add unit test to validate outstanding amount for update_outstanding_for_self checkbox enabled) ) for i, gle in enumerate(gl_entries): diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index c4baa4e4842..bd37d1c203a 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( @@ -111,7 +112,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, @@ -151,11 +152,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, [ @@ -168,6 +173,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 512877ab463af8d46442b95c6a468ca34c0de33b Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 8 Apr 2025 14:34:34 +0530 Subject: [PATCH 3/5] chore: resolve conflicts --- .../sales_invoice/test_sales_invoice.py | 89 ++----------------- erpnext/controllers/accounts_controller.py | 17 ---- 2 files changed, 9 insertions(+), 97 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 9917596f8be..9fc3e718f15 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -3860,63 +3860,6 @@ class TestSalesInvoice(FrappeTestCase): doc = frappe.get_doc("Project", project.name) self.assertEqual(doc.total_billed_amount, si.grand_total) - -<<<<<<< HEAD -def check_gl_entries(doc, voucher_no, expected_gle, posting_date): - gl_entries = frappe.db.sql( - """select account, debit, credit, posting_date - from `tabGL Entry` - where voucher_type='Sales Invoice' and voucher_no=%s and posting_date > %s - and is_cancelled = 0 - order by posting_date asc, account asc""", - (voucher_no, posting_date), - as_dict=1, -======= - si = create_sales_invoice(do_not_submit=True) - - project = frappe.new_doc("Project") - project.company = "_Test Company" - project.project_name = "Test Total Billed Amount" - project.save() - - si.project = project.name - si.items.append(copy(si.items[0])) - si.items.append(copy(si.items[0])) - si.items[0].project = project.name - si.items[1].project = project.name - # Not setting project on last item - si.items[1].insert() - si.items[2].insert() - si.submit() - - project.reload() - self.assertIsNone(si.items[2].project) - self.assertEqual(project.total_billed_amount, 300) - - def test_pos_returns_with_party_account_currency(self): - from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return - - pos_profile = make_pos_profile() - pos_profile.payments = [] - pos_profile.append("payments", {"default": 1, "mode_of_payment": "Cash"}) - pos_profile.save() - - pos = create_sales_invoice( - customer="_Test Customer USD", - currency="USD", - conversion_rate=86.595000000, - qty=2, - do_not_save=True, - ) - pos.is_pos = 1 - pos.pos_profile = pos_profile.name - pos.debit_to = "_Test Receivable USD - _TC" - pos.append("payments", {"mode_of_payment": "Cash", "account": "_Test Bank - _TC", "amount": 20.35}) - pos.save().submit() - - 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 @@ -3946,30 +3889,16 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date): 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 - invoice = create_sales_invoice(qty=10) - - return_doc = make_return_doc(invoice.doctype, invoice.name) - return_doc.items[0].qty = -10 - return_doc.save().submit() - - return_doc = make_return_doc(invoice.doctype, invoice.name) - return_doc.items[0].qty = 0 - - self.assertRaises(StockOverReturnError, return_doc.save) - - -def set_advance_flag(company, flag, default_account): - frappe.db.set_value( - "Company", - company, - { - "book_advance_payments_in_separate_party_account": flag, - "default_advance_received_account": default_account, - }, ->>>>>>> 7b0882600a (test: add unit test to validate outstanding amount for update_outstanding_for_self checkbox enabled) +def check_gl_entries(doc, voucher_no, expected_gle, posting_date): + gl_entries = frappe.db.sql( + """select account, debit, credit, posting_date + from `tabGL Entry` + where voucher_type='Sales Invoice' and voucher_no=%s and posting_date > %s + and is_cancelled = 0 + order by posting_date asc, account asc""", + (voucher_no, posting_date), + as_dict=1, ) for i, gle in enumerate(gl_entries): diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index a146628cd25..2ebd5b34ad9 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -252,23 +252,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 88facb752316b7131b792193a5e46fa229e2bcf8 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 8 Apr 2025 15:13:59 +0530 Subject: [PATCH 4/5] refactor: pass both 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 2ebd5b34ad9..41067b60252 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -190,7 +190,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)) From f62905f7a7d18fd1dddc0879047a3216a39d1758 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 8 Apr 2025 15:53:30 +0530 Subject: [PATCH 5/5] chore: pass individual range --- .../accounts_receivable/test_accounts_receivable.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index bd37d1c203a..9068bdff5a8 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -178,7 +178,10 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): "company": self.company, "based_on_payment_terms": 1, "report_date": today(), - "range": "30, 60, 90, 120", + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, "show_remarks": True, } @@ -210,7 +213,10 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): "company": self.company, "based_on_payment_terms": 1, "report_date": today(), - "range": "30, 60, 90, 120", + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, "show_remarks": True, }