From 0f428ed854c84d9882e5181aeb8e86faa0c5c492 Mon Sep 17 00:00:00 2001 From: Jatin3128 <140256508+Jatin3128@users.noreply.github.com> Date: Tue, 4 Aug 2026 15:59:44 +0530 Subject: [PATCH] fix(subscription): don't reactivate a cancelled subscription (#57774) * fix(subscription): don't reactivate a cancelled subscription set_subscription_status() unconditionally set status to Active once there was no outstanding invoice, even if the subscription had been intentionally cancelled. Paying off an invoice issued before cancellation (directly, or via the Payment Entry -> refresh hook) flipped a Cancelled subscription back to Active while cancelation_date stayed set. process()'s cancel_at_period_end check compared posting_date against getdate(self.end_date), and getdate(None) returns today, so an empty end_date was silently treated as "cancel now" on every scheduler run. Combined with the reactivation bug, this let a cancelled subscription toggle Cancelled -> Active on each run and generate another invoice at the next period boundary. Fixes #57761 * fix(test): compare normalized dates in subscription cancellation test cancelation_date read straight off an unsaved in-memory doc is a string from nowdate(), but the same field comes back as a datetime.date after reload(). Wrap both sides in getdate() so the comparison isn't type-sensitive. --- .../doctype/subscription/subscription.py | 5 ++- .../doctype/subscription/test_subscription.py | 32 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py index 5e4c32d82a4..033246941e3 100644 --- a/erpnext/accounts/doctype/subscription/subscription.py +++ b/erpnext/accounts/doctype/subscription/subscription.py @@ -278,6 +278,9 @@ class Subscription(Document): """ Sets the status of the `Subscription` """ + if self.status == STATUS_CANCELLED: + return + self._set_current_invoice_dates() if self.is_trialling(): self.status = STATUS_TRIALING @@ -673,7 +676,7 @@ class Subscription(Document): if self.cancel_at_period_end and ( getdate(posting_date) >= getdate(self.next_billing_period_end) - or getdate(posting_date) >= getdate(self.end_date) + or (self.end_date and getdate(posting_date) >= getdate(self.end_date)) ): self.cancel_subscription() diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py index 551bdb69166..ef96f5f150f 100644 --- a/erpnext/accounts/doctype/subscription/test_subscription.py +++ b/erpnext/accounts/doctype/subscription/test_subscription.py @@ -779,6 +779,38 @@ class TestSubscription(ERPNextTestSuite): subscription.reload() self.assertEqual(subscription.status, "Active") + def test_cancelled_subscription_stays_cancelled_after_payment_and_reprocess(self): + # https://github.com/frappe/erpnext/issues/57761 + subscription = create_subscription( + start_date=nowdate(), + generate_invoice_at="Prepaid (bill at period start)", + submit_invoice=1, + cancel_at_period_end=1, + ) + subscription.process(posting_date=nowdate()) + invoice = subscription.get_current_invoice() + self.assertGreater(invoice.outstanding_amount, 0) + + subscription.cancel_subscription() + self.assertEqual(subscription.status, "Cancelled") + cancelation_date = getdate(subscription.cancelation_date) + self.assertIsNotNone(cancelation_date) + + payment_entry = get_payment_entry(invoice.doctype, invoice.name, bank_account="_Test Bank - _TC") + payment_entry.reference_no = "12345" + payment_entry.reference_date = nowdate() + payment_entry.submit() + + subscription.reload() + self.assertEqual(subscription.status, "Cancelled") + self.assertEqual(getdate(subscription.cancelation_date), cancelation_date) + + invoice_count = len(subscription.invoices) + subscription.process() + subscription.reload() + self.assertEqual(subscription.status, "Cancelled") + self.assertEqual(len(subscription.invoices), invoice_count) + def test_first_invoice_generated_on_create_for_prepaid(self): subscription = create_subscription( start_date=nowdate(),