From 8f37926ca89cf477fab2adaa6a2ec708a9e35e89 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Thu, 1 Mar 2018 04:50:04 +0100 Subject: [PATCH] add ability to cancel, restart and refresh subscription --- .../doctype/subscriptions/subscriptions.js | 69 ++++++++++ .../doctype/subscriptions/subscriptions.py | 55 ++++++-- .../subscriptions/test_subscriptions.py | 118 ++++++++++++++++++ 3 files changed, 235 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.js b/erpnext/accounts/doctype/subscriptions/subscriptions.js index 14eb9b60fe..ae572d24b7 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.js +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.js @@ -3,6 +3,75 @@ frappe.ui.form.on('Subscriptions', { refresh: function(frm) { + if(!frm.is_new()){ + if(frm.doc.status !== 'Canceled'){ + frm.add_custom_button( + __('Cancel Subscription'), + () => frm.events.cancel_this_subscription(frm) + ); + frm.add_custom_button( + __('Fetch Subscription Updates'), + () => frm.events.get_subscription_updates(frm) + ); + } + else if(frm.doc.status === 'Canceled'){ + frm.add_custom_button( + __('Restart Subscription'), + () => frm.events.renew_this_subscription(frm) + ); + } + } + }, + cancel_this_subscription: function(frm) { + const doc = frm.doc; + frappe.confirm( + __('This action will stop future billing. Are you sure you want to cancel this subscription?'), + function() { + frappe.call({ + method: + "erpnext.accounts.doctype.subscriptions.subscriptions.cancel_subscription", + args: {name: doc.name}, + callback: function(data){ + if(!data.exc){ + frm.reload_doc(); + } + } + }); + } + ); + }, + + renew_this_subscription: function(frm) { + const doc = frm.doc; + frappe.confirm( + __('You will lose records of previously generated invoices. Are you sure you want to restart this subscription?'), + function() { + frappe.call({ + method: + "erpnext.accounts.doctype.subscriptions.subscriptions.restart_subscription", + args: {name: doc.name}, + callback: function(data){ + if(!data.exc){ + frm.reload_doc(); + } + } + }); + } + ); + }, + + get_subscription_updates: function(frm) { + const doc = frm.doc; + frappe.call({ + method: + "erpnext.accounts.doctype.subscriptions.subscriptions.get_subscription_updates", + args: {name: doc.name}, + callback: function(data){ + if(!data.exc){ + frm.reload_doc(); + } + } + }); } }); diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.py b/erpnext/accounts/doctype/subscriptions/subscriptions.py index 123cd328c5..cd43a9cca4 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.py @@ -14,8 +14,8 @@ class Subscriptions(Document): # update start just before the subscription doc is created self.update_subscription_period() - def update_subscription_period(self): - self.set_current_invoice_start() + def update_subscription_period(self, date=None): + self.set_current_invoice_start(date) self.set_current_invoice_end() def set_current_invoice_start(self, date=None): @@ -228,16 +228,19 @@ class Subscriptions(Document): """ if self.status == 'Active': self.process_for_active() - elif self.status == 'Past Due Date': + elif self.status in ['Past Due Date', 'Unpaid']: self.process_for_past_due_date() - self.save() - # process_for_unpaid() + + if self.status != 'Canceled': + self.save() def process_for_active(self): if getdate(nowdate()) > getdate(self.current_invoice_end) and not self.has_outstanding_invoice(): self.generate_invoice() + if self.current_invoice_is_past_due(): + self.status = 'Past Due Date' - if self.current_invoice_is_past_due(): + if self.current_invoice_is_past_due() and getdate(nowdate()) > getdate(self.current_invoice_end): self.status = 'Past Due Date' def process_for_past_due_date(self): @@ -247,7 +250,7 @@ class Subscriptions(Document): else: if self.is_not_outstanding(current_invoice): self.status = 'Active' - self.update_subscription_period() + self.update_subscription_period(nowdate()) else: self.set_status_grace_period() @@ -261,3 +264,41 @@ class Subscriptions(Document): else: return not self.is_not_outstanding(current_invoice) return True + + def cancel_subscription(self): + """ + This sets the subscription as cancelled. It will stop invoices from being generated + but it will not affect already created invoices. + """ + self.status = 'Canceled' + self.cancelation_date = nowdate() + self.save() + + def restart_subscription(self): + """ + This sets the subscription as active. The subscription will be made to be like a new + subscription but new trial periods will not be allowed. + """ + self.status = 'Active' + self.cancelation_date = None + self.update_subscription_period(nowdate()) + self.invoices = [] + self.save() + + +@frappe.whitelist() +def cancel_subscription(name): + subscription = frappe.get_doc('Subscriptions', name) + subscription.cancel_subscription() + + +@frappe.whitelist() +def restart_subscription(name): + subscription = frappe.get_doc('Subscriptions', name) + subscription.restart_subscription() + + +@frappe.whitelist() +def get_subscription_updates(name): + subscription = frappe.get_doc('Subscriptions', name) + subscription.process() \ No newline at end of file diff --git a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py index a3413c7b1e..a0f940098b 100644 --- a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py @@ -267,5 +267,123 @@ class TestSubscriptions(unittest.TestCase): subscription.delete() + def test_subcription_cancelation(self): + subscription = frappe.new_doc('Subscriptions') + subscription.subscriber = '_Test Customer' + subscription.append('plans', {'plan': '_Test Plan Name'}) + subscription.save() + subscription.cancel_subscription() + + self.assertEqual(subscription.status, 'Canceled') + + subscription.delete() + + def test_subcription_cancelation_and_process(self): + settings = frappe.get_single('Subscription Settings') + default_grace_period_action = settings.cancel_after_grace + settings.cancel_after_grace = 1 + settings.save() + + subscription = frappe.new_doc('Subscriptions') + subscription.subscriber = '_Test Customer' + subscription.append('plans', {'plan': '_Test Plan Name'}) + subscription.insert() + subscription.set_current_invoice_start('2018-01-01') + subscription.set_current_invoice_end() + subscription.process() # generate first invoice + invoices = len(subscription.invoices) + + self.assertEqual(subscription.status, 'Past Due Date') + self.assertEqual(len(subscription.invoices), invoices) + + subscription.cancel_subscription() + self.assertEqual(subscription.status, 'Canceled') + self.assertEqual(len(subscription.invoices), invoices) + + subscription.process() + self.assertEqual(subscription.status, 'Canceled') + self.assertEqual(len(subscription.invoices), invoices) + + subscription.process() + self.assertEqual(subscription.status, 'Canceled') + self.assertEqual(len(subscription.invoices), invoices) + + settings.cancel_after_grace = default_grace_period_action + settings.save() + subscription.delete() + + def test_subscription_restart_and_process(self): + settings = frappe.get_single('Subscription Settings') + default_grace_period_action = settings.cancel_after_grace + settings.grace_period = 0 + settings.cancel_after_grace = 0 + settings.save() + + subscription = frappe.new_doc('Subscriptions') + subscription.subscriber = '_Test Customer' + subscription.append('plans', {'plan': '_Test Plan Name'}) + subscription.insert() + subscription.set_current_invoice_start('2018-01-01') + subscription.set_current_invoice_end() + subscription.process() # generate first invoice + + self.assertEqual(subscription.status, 'Past Due Date') + + subscription.process() + self.assertEqual(subscription.status, 'Unpaid') + + subscription.cancel_subscription() + self.assertEqual(subscription.status, 'Canceled') + + subscription.restart_subscription() + self.assertEqual(subscription.status, 'Active') + self.assertEqual(len(subscription.invoices), 0) + + subscription.process() + self.assertEqual(subscription.status, 'Active') + self.assertEqual(len(subscription.invoices), 0) + + subscription.process() + self.assertEqual(subscription.status, 'Active') + self.assertEqual(len(subscription.invoices), 0) + + settings.cancel_after_grace = default_grace_period_action + settings.save() + subscription.delete() + + def test_subscription_unpaid_back_to_active(self): + settings = frappe.get_single('Subscription Settings') + default_grace_period_action = settings.cancel_after_grace + settings.cancel_after_grace = 0 + settings.save() + + subscription = frappe.new_doc('Subscriptions') + subscription.subscriber = '_Test Customer' + subscription.append('plans', {'plan': '_Test Plan Name'}) + subscription.insert() + subscription.set_current_invoice_start('2018-01-01') + subscription.set_current_invoice_end() + subscription.process() # generate first invoice + + self.assertEqual(subscription.status, 'Past Due Date') + + subscription.process() + # This should change status to Canceled since grace period is 0 + self.assertEqual(subscription.status, 'Unpaid') + + invoice = subscription.get_current_invoice() + invoice.db_set('outstanding_amount', 0) + invoice.db_set('status', 'Paid') + + subscription.process() + self.assertEqual(subscription.status, 'Active') + + subscription.process() + self.assertEqual(subscription.status, 'Active') + + settings.cancel_after_grace = default_grace_period_action + settings.save() + subscription.delete() + def test_subscription_creation_with_multiple_plans(self): pass