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.
This commit is contained in:
Jatin3128
2026-08-04 15:59:44 +05:30
committed by GitHub
parent 3dd01e5120
commit 0f428ed854
2 changed files with 36 additions and 1 deletions

View File

@@ -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()

View File

@@ -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(),