diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py index 684bb6fd22a..fdb6c676ee6 100644 --- a/erpnext/accounts/doctype/subscription/subscription.py +++ b/erpnext/accounts/doctype/subscription/subscription.py @@ -222,7 +222,11 @@ class Subscription(Document): """ if self.is_trialling(): self.status = "Trialing" - elif self.status == "Active" and self.end_date and getdate(posting_date) > getdate(self.end_date): + elif ( + not self.has_outstanding_invoice() + and self.end_date + and getdate(posting_date) > getdate(self.end_date) + ): self.status = "Completed" elif self.is_past_grace_period(): self.status = self.get_status_for_past_grace_period() @@ -562,6 +566,17 @@ class Subscription(Document): self.current_invoice_start, self.current_invoice_end ) and self.can_generate_new_invoice(posting_date): self.generate_invoice(posting_date=posting_date) + if self.end_date: + next_start = add_days(self.current_invoice_end, 1) + + if getdate(next_start) > getdate(self.end_date): + if self.cancel_at_period_end: + self.cancel_subscription() + else: + self.set_subscription_status(posting_date=posting_date) + + self.save() + return self.update_subscription_period(add_days(self.current_invoice_end, 1)) elif posting_date and getdate(posting_date) > getdate(self.current_invoice_end): self.update_subscription_period() diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py index d9459948b58..80a167f07c2 100644 --- a/erpnext/accounts/doctype/subscription/test_subscription.py +++ b/erpnext/accounts/doctype/subscription/test_subscription.py @@ -17,6 +17,7 @@ from frappe.utils.data import ( nowdate, ) +from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.subscription.subscription import get_prorata_factor EXTRA_TEST_RECORD_DEPENDENCIES = ("UOM", "Item Group", "Item") @@ -583,6 +584,105 @@ class TestSubscription(IntegrationTestCase): subscription.process(nowdate()) self.assertEqual(len(subscription.invoices), 1) + def test_subscription_auto_cancellation(self): + create_plan( + plan_name="_Test plan name 10", + cost=80, + currency="INR", + billing_interval="Day", + billing_interval_count=3, + ) + start_date = getdate("2025-01-01") + subscription = create_subscription( + start_date=start_date, + end_date=add_days(start_date, 8), + cancel_at_period_end=1, + generate_new_invoices_past_due_date=1, + generate_invoice_at="Beginning of the current subscription period", + plans=[{"plan": "_Test plan name 10", "qty": 1}], + ) + subscription.process(posting_date=add_days(start_date, 2)) + self.assertEqual(len(subscription.invoices), 1) + + subscription.process(posting_date=add_days(start_date, 5)) + self.assertEqual(len(subscription.invoices), 2) + + subscription.process(posting_date=add_days(start_date, 8)) + self.assertEqual(len(subscription.invoices), 3) + self.assertEqual(subscription.status, "Cancelled") + + def test_subscription_auto_cancellation_uneven_cycle(self): + create_plan( + plan_name="_Test plan name 10", + cost=80, + currency="INR", + billing_interval="Day", + billing_interval_count=3, + ) + start_date = getdate("2025-01-01") + subscription = create_subscription( + start_date=start_date, + end_date=add_days(start_date, 6), + cancel_at_period_end=1, + generate_new_invoices_past_due_date=1, + generate_invoice_at="Beginning of the current subscription period", + plans=[{"plan": "_Test plan name 10", "qty": 1}], + ) + + subscription.process(posting_date=add_days(start_date, 2)) + self.assertEqual(len(subscription.invoices), 1) + + subscription.process(posting_date=add_days(start_date, 5)) + self.assertEqual(len(subscription.invoices), 2) + + # partial last cycle invoice + subscription.process(posting_date=add_days(start_date, 6)) + self.assertEqual(len(subscription.invoices), 3) + + self.assertEqual(subscription.status, "Cancelled") + + self.assertRaises(frappe.ValidationError, subscription.process, posting_date=add_days(start_date, 7)) + + def test_subscription_auto_completion(self): + create_plan( + plan_name="_Test Plan 3 Day", + cost=100, + billing_interval="Day", + billing_interval_count=3, + currency="INR", + ) + + start_date = getdate("2025-01-01") + end_date = add_days(start_date, 6) + + subscription = create_subscription( + start_date=start_date, + end_date=end_date, + party_type="Customer", + party="_Test Customer", + generate_invoice_at="Beginning of the current subscription period", + generate_new_invoices_past_due_date=1, + plans=[{"plan": "_Test Plan 3 Day", "qty": 1}], + ) + + for day in range(0, 10): + if subscription.status == "Cancelled": + break + subscription.process(posting_date=add_days(start_date, day)) + + invoices = frappe.get_all( + "Sales Invoice", + filters={"subscription": subscription.name, "docstatus": 1}, + fields=["name", "from_date", "to_date"], + order_by="from_date asc", + ) + for invoice in invoices: + pi = get_payment_entry("Sales Invoice", invoice.name) + pi.submit() + # After processing through all days, subscription should be completed + subscription.process(posting_date=add_days(end_date, 1)) + self.assertEqual(subscription.status, "Completed") + def make_plans(): create_plan(plan_name="_Test Plan Name", cost=900, currency="INR") @@ -659,6 +759,7 @@ def create_subscription(**kwargs): subscription.trial_period_start = kwargs.get("trial_period_start") subscription.trial_period_end = kwargs.get("trial_period_end") subscription.start_date = kwargs.get("start_date") + subscription.end_date = kwargs.get("end_date") subscription.generate_invoice_at = kwargs.get("generate_invoice_at") subscription.additional_discount_percentage = kwargs.get("additional_discount_percentage") subscription.additional_discount_amount = kwargs.get("additional_discount_amount") @@ -667,6 +768,7 @@ def create_subscription(**kwargs): subscription.submit_invoice = kwargs.get("submit_invoice") subscription.days_until_due = kwargs.get("days_until_due") subscription.number_of_days = kwargs.get("number_of_days") + subscription.cancel_at_period_end = kwargs.get("cancel_at_period_end") if not kwargs.get("plans"): subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})