From cb6fc640cebbbde627755e341a02070c61e2888e Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 19 Jun 2026 15:40:51 +0530 Subject: [PATCH] refactor(sales_invoice): simplify StatusService.set_status and cover set_indicator set_status was a single status-resolution cascade (cyclomatic complexity C/19). Extract the submitted-invoice resolution into _submitted_status (with the invoice-discounting suffix) and _payment_status (guard clauses), leaving the cancelled/draft quirks inline. set_status drops C/19 -> B/7; no C-rank function remains in the module. Add a characterization test for set_indicator, which was 93% untested - the portal indicator colour/title for credit-note / unpaid / overdue / return / paid states. Behaviour is unchanged (status and invoice-discounting suites green). --- .../doctype/sales_invoice/services/status.py | 65 ++++++++++--------- .../sales_invoice/test_sales_invoice.py | 21 ++++++ 2 files changed, 57 insertions(+), 29 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/services/status.py b/erpnext/accounts/doctype/sales_invoice/services/status.py index ae6e6754451..c6402c559ef 100644 --- a/erpnext/accounts/doctype/sales_invoice/services/status.py +++ b/erpnext/accounts/doctype/sales_invoice/services/status.py @@ -21,45 +21,52 @@ class StatusService: doc.status = "Draft" return - outstanding_amount = flt(doc.outstanding_amount, doc.precision("outstanding_amount")) - total = get_total_in_party_account_currency(doc) - if not status: if doc.docstatus == 2: status = "Cancelled" elif doc.docstatus == 1: - if doc.is_internal_transfer(): - doc.status = "Internal Transfer" - elif is_overdue(doc, total): - doc.status = "Overdue" - elif 0 < outstanding_amount < total: - doc.status = "Partly Paid" - elif outstanding_amount > 0 and getdate(doc.due_date) >= getdate(): - doc.status = "Unpaid" - elif doc.is_return == 0 and frappe.db.get_value( - "Sales Invoice", {"is_return": 1, "return_against": doc.name, "docstatus": 1} - ): - doc.status = "Credit Note Issued" - elif doc.is_return == 1: - doc.status = "Return" - elif outstanding_amount <= 0: - doc.status = "Paid" - else: - doc.status = "Submitted" - - if ( - doc.status in ("Unpaid", "Partly Paid", "Overdue") - and doc.is_discounted - and get_discounting_status(doc.name) == "Disbursed" - ): - doc.status += " and Discounted" - + doc.status = self._get_submitted_status() else: doc.status = "Draft" if update: doc.db_set("status", doc.status, update_modified=update_modified) + def _get_submitted_status(self) -> str: + """Status of a submitted invoice, with the invoice-discounting suffix applied.""" + doc = self.doc + outstanding_amount = flt(doc.outstanding_amount, doc.precision("outstanding_amount")) + total = get_total_in_party_account_currency(doc) + + status = self._get_payment_status(outstanding_amount, total) + if ( + status in ("Unpaid", "Partly Paid", "Overdue") + and doc.is_discounted + and get_discounting_status(doc.name) == "Disbursed" + ): + status += " and Discounted" + return status + + def _get_payment_status(self, outstanding_amount: float, total: float) -> str: + doc = self.doc + if doc.is_internal_transfer(): + return "Internal Transfer" + if is_overdue(doc, total): + return "Overdue" + if 0 < outstanding_amount < total: + return "Partly Paid" + if outstanding_amount > 0 and getdate(doc.due_date) >= getdate(): + return "Unpaid" + if doc.is_return == 0 and frappe.db.get_value( + "Sales Invoice", {"is_return": 1, "return_against": doc.name, "docstatus": 1} + ): + return "Credit Note Issued" + if doc.is_return == 1: + return "Return" + if outstanding_amount <= 0: + return "Paid" + return "Submitted" + def set_indicator(self) -> None: doc = self.doc if doc.outstanding_amount < 0: diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 713bf278c66..e1eac774faf 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -3880,6 +3880,27 @@ class TestSalesInvoice(ERPNextTestSuite): party_link.delete() + def test_status_indicator(self): + from erpnext.accounts.doctype.sales_invoice.services.status import StatusService + + si = create_sales_invoice(do_not_save=True) + cases = [ + # outstanding, due_date, is_return -> indicator color, title + (-50, nowdate(), 0, "gray", "Credit Note Issued"), + (100, add_days(nowdate(), 5), 0, "orange", "Unpaid"), + (100, add_days(nowdate(), -5), 0, "red", "Overdue"), + (0, nowdate(), 1, "gray", "Return"), + (0, nowdate(), 0, "green", "Paid"), + ] + for outstanding, due_date, is_return, color, title in cases: + with self.subTest(title=title): + si.outstanding_amount = outstanding + si.due_date = due_date + si.is_return = is_return + StatusService(si).set_indicator() + self.assertEqual(si.indicator_color, color) + self.assertEqual(si.indicator_title, title) + def test_payment_statuses(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry