feat: subscription refactor (#30963)

* feat: subscription refactor

* fix: linter changes

* chore: linter changes

* chore: linter changes

* chore: Update tests

* chore: Remove commits

---------

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
index d8759e9..0599e19 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
@@ -167,6 +167,7 @@
   "column_break_63",
   "unrealized_profit_loss_account",
   "subscription_section",
+  "subscription",
   "auto_repeat",
   "update_auto_repeat_reference",
   "column_break_114",
@@ -1424,6 +1425,12 @@
    "read_only": 1
   },
   {
+   "fieldname": "subscription",
+   "fieldtype": "Link",
+   "label": "Subscription",
+   "options": "Subscription"
+  },
+  {
    "default": "0",
    "fieldname": "is_old_subcontracting_flow",
    "fieldtype": "Check",
@@ -1577,7 +1584,7 @@
  "idx": 204,
  "is_submittable": 1,
  "links": [],
- "modified": "2023-07-04 17:22:59.145031",
+ "modified": "2023-07-25 17:22:59.145031",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Purchase Invoice",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index f0d3f72..7581366 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -194,6 +194,7 @@
   "select_print_heading",
   "language",
   "subscription_section",
+  "subscription",
   "from_date",
   "auto_repeat",
   "column_break_140",
@@ -2018,6 +2019,12 @@
    "read_only": 1
   },
   {
+   "fieldname": "subscription",
+   "fieldtype": "Link",
+   "label": "Subscription",
+   "options": "Subscription"
+  },
+  {
    "default": "0",
    "depends_on": "eval: doc.apply_discount_on == \"Grand Total\"",
    "fieldname": "is_cash_or_non_trade_discount",
@@ -2157,7 +2164,7 @@
    "link_fieldname": "consolidated_invoice"
   }
  ],
- "modified": "2023-06-21 16:02:18.988799",
+ "modified": "2023-07-25 16:02:18.988799",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Sales Invoice",
diff --git a/erpnext/accounts/doctype/subscription/subscription.js b/erpnext/accounts/doctype/subscription/subscription.js
index 1a90664..ae789b5 100644
--- a/erpnext/accounts/doctype/subscription/subscription.js
+++ b/erpnext/accounts/doctype/subscription/subscription.js
@@ -2,16 +2,16 @@
 // For license information, please see license.txt
 
 frappe.ui.form.on('Subscription', {
-	setup: function(frm) {
-		frm.set_query('party_type', function() {
+	setup: function (frm) {
+		frm.set_query('party_type', function () {
 			return {
-				filters : {
+				filters: {
 					name: ['in', ['Customer', 'Supplier']]
 				}
 			}
 		});
 
-		frm.set_query('cost_center', function() {
+		frm.set_query('cost_center', function () {
 			return {
 				filters: {
 					company: frm.doc.company
@@ -20,76 +20,60 @@
 		});
 	},
 
-	refresh: function(frm) {
-		if(!frm.is_new()){
-			if(frm.doc.status !== 'Cancelled'){
-				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 === 'Cancelled'){
-				frm.add_custom_button(
-					__('Restart Subscription'),
-					() => frm.events.renew_this_subscription(frm)
-				);
-			}
+	refresh: function (frm) {
+		if (frm.is_new()) return;
+
+		if (frm.doc.status !== 'Cancelled') {
+			frm.add_custom_button(
+				__('Fetch Subscription Updates'),
+				() => frm.trigger('get_subscription_updates'),
+				__('Actions')
+			);
+
+			frm.add_custom_button(
+				__('Cancel Subscription'),
+				() => frm.trigger('cancel_this_subscription'),
+				__('Actions')
+			);
+		} else if (frm.doc.status === 'Cancelled') {
+			frm.add_custom_button(
+				__('Restart Subscription'),
+				() => frm.trigger('renew_this_subscription'),
+				__('Actions')
+			);
 		}
 	},
 
-	cancel_this_subscription: function(frm) {
-		const doc = frm.doc;
+	cancel_this_subscription: function (frm) {
 		frappe.confirm(
 			__('This action will stop future billing. Are you sure you want to cancel this subscription?'),
-			function() {
-				frappe.call({
-					method:
-					"erpnext.accounts.doctype.subscription.subscription.cancel_subscription",
-					args: {name: doc.name},
-					callback: function(data){
-						if(!data.exc){
-							frm.reload_doc();
-						}
+			() => {
+				frm.call('cancel_subscription').then(r => {
+					if (!r.exec) {
+						frm.reload_doc();
 					}
 				});
 			}
 		);
 	},
 
-	renew_this_subscription: function(frm) {
-		const doc = frm.doc;
+	renew_this_subscription: function (frm) {
 		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.subscription.subscription.restart_subscription",
-					args: {name: doc.name},
-					callback: function(data){
-						if(!data.exc){
-							frm.reload_doc();
-						}
+			__('Are you sure you want to restart this subscription?'),
+			() => {
+				frm.call('restart_subscription').then(r => {
+					if (!r.exec) {
+						frm.reload_doc();
 					}
 				});
 			}
 		);
 	},
 
-	get_subscription_updates: function(frm) {
-		const doc = frm.doc;
-		frappe.call({
-			method:
-			"erpnext.accounts.doctype.subscription.subscription.get_subscription_updates",
-			args: {name: doc.name},
-			freeze: true,
-			callback: function(data){
-				if(!data.exc){
-					frm.reload_doc();
-				}
+	get_subscription_updates: function (frm) {
+		frm.call('process').then(r => {
+			if (!r.exec) {
+				frm.reload_doc();
 			}
 		});
 	}
diff --git a/erpnext/accounts/doctype/subscription/subscription.json b/erpnext/accounts/doctype/subscription/subscription.json
index c4e4be7..c15aa1e 100644
--- a/erpnext/accounts/doctype/subscription/subscription.json
+++ b/erpnext/accounts/doctype/subscription/subscription.json
@@ -19,6 +19,7 @@
   "trial_period_end",
   "follow_calendar_months",
   "generate_new_invoices_past_due_date",
+  "submit_invoice",
   "column_break_11",
   "current_invoice_start",
   "current_invoice_end",
@@ -35,12 +36,8 @@
   "cb_2",
   "additional_discount_percentage",
   "additional_discount_amount",
-  "sb_3",
-  "submit_invoice",
-  "invoices",
   "accounting_dimensions_section",
-  "cost_center",
-  "dimension_col_break"
+  "cost_center"
  ],
  "fields": [
   {
@@ -163,29 +160,12 @@
    "label": "Additional DIscount Amount"
   },
   {
-   "depends_on": "eval:doc.invoices",
-   "fieldname": "sb_3",
-   "fieldtype": "Section Break",
-   "label": "Invoices"
-  },
-  {
-   "collapsible": 1,
-   "fieldname": "invoices",
-   "fieldtype": "Table",
-   "label": "Invoices",
-   "options": "Subscription Invoice"
-  },
-  {
    "collapsible": 1,
    "fieldname": "accounting_dimensions_section",
    "fieldtype": "Section Break",
    "label": "Accounting Dimensions"
   },
   {
-   "fieldname": "dimension_col_break",
-   "fieldtype": "Column Break"
-  },
-  {
    "fieldname": "party_type",
    "fieldtype": "Link",
    "label": "Party Type",
@@ -259,15 +239,27 @@
    "default": "1",
    "fieldname": "submit_invoice",
    "fieldtype": "Check",
-   "label": "Submit Invoice Automatically"
+   "label": "Submit Generated Invoices"
   }
  ],
  "index_web_pages_for_search": 1,
- "links": [],
- "modified": "2021-04-19 15:24:27.550797",
+ "links": [
+  {
+   "group": "Buying",
+   "link_doctype": "Purchase Invoice",
+   "link_fieldname": "subscription"
+  },
+  {
+   "group": "Selling",
+   "link_doctype": "Sales Invoice",
+   "link_fieldname": "subscription"
+  }
+ ],
+ "modified": "2022-02-18 23:24:57.185054",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Subscription",
+ "naming_rule": "Expression (old style)",
  "owner": "Administrator",
  "permissions": [
   {
@@ -309,5 +301,6 @@
  ],
  "sort_field": "modified",
  "sort_order": "DESC",
+ "states": [],
  "track_changes": 1
 }
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py
index 8708342..bbcade1 100644
--- a/erpnext/accounts/doctype/subscription/subscription.py
+++ b/erpnext/accounts/doctype/subscription/subscription.py
@@ -2,14 +2,17 @@
 # For license information, please see license.txt
 
 
+from datetime import datetime
+from typing import Dict, List, Optional, Union
+
 import frappe
 from frappe import _
 from frappe.model.document import Document
 from frappe.utils.data import (
 	add_days,
+	add_months,
 	add_to_date,
 	cint,
-	cstr,
 	date_diff,
 	flt,
 	get_last_day,
@@ -17,8 +20,7 @@
 	nowdate,
 )
 
-import erpnext
-from erpnext import get_default_company
+from erpnext import get_default_company, get_default_cost_center
 from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
 	get_accounting_dimensions,
 )
@@ -26,33 +28,39 @@
 from erpnext.accounts.party import get_party_account_currency
 
 
+class InvoiceCancelled(frappe.ValidationError):
+	pass
+
+
+class InvoiceNotCancelled(frappe.ValidationError):
+	pass
+
+
 class Subscription(Document):
 	def before_insert(self):
 		# update start just before the subscription doc is created
 		self.update_subscription_period(self.start_date)
 
-	def update_subscription_period(self, date=None, return_date=False):
+	def update_subscription_period(self, date: Optional[Union[datetime.date, str]] = None):
 		"""
 		Subscription period is the period to be billed. This method updates the
 		beginning of the billing period and end of the billing period.
-
 		The beginning of the billing period is represented in the doctype as
 		`current_invoice_start` and the end of the billing period is represented
 		as `current_invoice_end`.
-
-		If return_date is True, it wont update the start and end dates.
-		This is implemented to get the dates to check if is_current_invoice_generated
 		"""
+		self.current_invoice_start = self.get_current_invoice_start(date)
+		self.current_invoice_end = self.get_current_invoice_end(self.current_invoice_start)
+
+	def _get_subscription_period(self, date: Optional[Union[datetime.date, str]] = None):
 		_current_invoice_start = self.get_current_invoice_start(date)
 		_current_invoice_end = self.get_current_invoice_end(_current_invoice_start)
 
-		if return_date:
-			return _current_invoice_start, _current_invoice_end
+		return _current_invoice_start, _current_invoice_end
 
-		self.current_invoice_start = _current_invoice_start
-		self.current_invoice_end = _current_invoice_end
-
-	def get_current_invoice_start(self, date=None):
+	def get_current_invoice_start(
+		self, date: Optional[Union[datetime.date, str]] = None
+	) -> Union[datetime.date, str]:
 		"""
 		This returns the date of the beginning of the current billing period.
 		If the `date` parameter is not given , it will be automatically set as today's
@@ -75,13 +83,13 @@
 
 		return _current_invoice_start
 
-	def get_current_invoice_end(self, date=None):
+	def get_current_invoice_end(
+		self, date: Optional[Union[datetime.date, str]] = None
+	) -> Union[datetime.date, str]:
 		"""
 		This returns the date of the end of the current billing period.
-
 		If the subscription is in trial period, it will be set as the end of the
 		trial period.
-
 		If is not in a trial period, it will be `x` days from the beginning of the
 		current billing period where `x` is the billing interval from the
 		`Subscription Plan` in the `Subscription`.
@@ -105,24 +113,13 @@
 				_current_invoice_end = get_last_day(date)
 
 			if self.follow_calendar_months:
+				# Sets the end date
+				# eg if date is 17-Feb-2022, the invoice will be generated per month ie
+				# the invoice will be created from 17 Feb to 28 Feb
 				billing_info = self.get_billing_cycle_and_interval()
 				billing_interval_count = billing_info[0]["billing_interval_count"]
-				calendar_months = get_calendar_months(billing_interval_count)
-				calendar_month = 0
-				current_invoice_end_month = getdate(_current_invoice_end).month
-				current_invoice_end_year = getdate(_current_invoice_end).year
-
-				for month in calendar_months:
-					if month <= current_invoice_end_month:
-						calendar_month = month
-
-				if cint(calendar_month - billing_interval_count) <= 0 and getdate(date).month != 1:
-					calendar_month = 12
-					current_invoice_end_year -= 1
-
-				_current_invoice_end = get_last_day(
-					cstr(current_invoice_end_year) + "-" + cstr(calendar_month) + "-01"
-				)
+				_end = add_months(getdate(date), billing_interval_count - 1)
+				_current_invoice_end = get_last_day(_end)
 
 			if self.end_date and getdate(_current_invoice_end) > getdate(self.end_date):
 				_current_invoice_end = self.end_date
@@ -130,7 +127,7 @@
 		return _current_invoice_end
 
 	@staticmethod
-	def validate_plans_billing_cycle(billing_cycle_data):
+	def validate_plans_billing_cycle(billing_cycle_data: List[Dict[str, str]]) -> None:
 		"""
 		Makes sure that all `Subscription Plan` in the `Subscription` have the
 		same billing interval
@@ -138,10 +135,9 @@
 		if billing_cycle_data and len(billing_cycle_data) != 1:
 			frappe.throw(_("You can only have Plans with the same billing cycle in a Subscription"))
 
-	def get_billing_cycle_and_interval(self):
+	def get_billing_cycle_and_interval(self) -> List[Dict[str, str]]:
 		"""
 		Returns a dict representing the billing interval and cycle for this `Subscription`.
-
 		You shouldn't need to call this directly. Use `get_billing_cycle` instead.
 		"""
 		plan_names = [plan.plan for plan in self.plans]
@@ -156,72 +152,65 @@
 
 		return billing_info
 
-	def get_billing_cycle_data(self):
+	def get_billing_cycle_data(self) -> Dict[str, int]:
 		"""
 		Returns dict contain the billing cycle data.
-
 		You shouldn't need to call this directly. Use `get_billing_cycle` instead.
 		"""
 		billing_info = self.get_billing_cycle_and_interval()
+		if not billing_info:
+			return None
 
-		self.validate_plans_billing_cycle(billing_info)
+		data = dict()
+		interval = billing_info[0]["billing_interval"]
+		interval_count = billing_info[0]["billing_interval_count"]
 
-		if billing_info:
-			data = dict()
-			interval = billing_info[0]["billing_interval"]
-			interval_count = billing_info[0]["billing_interval_count"]
-			if interval not in ["Day", "Week"]:
-				data["days"] = -1
-			if interval == "Day":
-				data["days"] = interval_count - 1
-			elif interval == "Month":
-				data["months"] = interval_count
-			elif interval == "Year":
-				data["years"] = interval_count
-			# todo: test week
-			elif interval == "Week":
-				data["days"] = interval_count * 7 - 1
+		if interval not in ["Day", "Week"]:
+			data["days"] = -1
 
-			return data
+		if interval == "Day":
+			data["days"] = interval_count - 1
+		elif interval == "Week":
+			data["days"] = interval_count * 7 - 1
+		elif interval == "Month":
+			data["months"] = interval_count
+		elif interval == "Year":
+			data["years"] = interval_count
 
-	def set_status_grace_period(self):
-		"""
-		Sets the `Subscription` `status` based on the preference set in `Subscription Settings`.
+		return data
 
-		Used when the `Subscription` needs to decide what to do after the current generated
-		invoice is past it's due date and grace period.
-		"""
-		subscription_settings = frappe.get_single("Subscription Settings")
-		if self.status == "Past Due Date" and self.is_past_grace_period():
-			self.status = "Cancelled" if cint(subscription_settings.cancel_after_grace) else "Unpaid"
-
-	def set_subscription_status(self):
+	def set_subscription_status(self) -> None:
 		"""
 		Sets the status of the `Subscription`
 		"""
 		if self.is_trialling():
 			self.status = "Trialling"
-		elif self.status == "Active" and self.end_date and getdate() > getdate(self.end_date):
+		elif (
+			self.status == "Active"
+			and self.end_date
+			and getdate(frappe.flags.current_date) > getdate(self.end_date)
+		):
 			self.status = "Completed"
 		elif self.is_past_grace_period():
-			subscription_settings = frappe.get_single("Subscription Settings")
-			self.status = "Cancelled" if cint(subscription_settings.cancel_after_grace) else "Unpaid"
+			self.status = self.get_status_for_past_grace_period()
+			self.cancelation_date = (
+				getdate(frappe.flags.current_date) if self.status == "Cancelled" else None
+			)
 		elif self.current_invoice_is_past_due() and not self.is_past_grace_period():
 			self.status = "Past Due Date"
-		elif not self.has_outstanding_invoice():
+		elif not self.has_outstanding_invoice() or self.is_new_subscription():
 			self.status = "Active"
-		elif self.is_new_subscription():
-			self.status = "Active"
+
 		self.save()
 
-	def is_trialling(self):
+	def is_trialling(self) -> bool:
 		"""
 		Returns `True` if the `Subscription` is in trial period.
 		"""
 		return not self.period_has_passed(self.trial_period_end) and self.is_new_subscription()
 
 	@staticmethod
-	def period_has_passed(end_date):
+	def period_has_passed(end_date: Union[str, datetime.date]) -> bool:
 		"""
 		Returns true if the given `end_date` has passed
 		"""
@@ -229,61 +218,59 @@
 		if not end_date:
 			return True
 
-		end_date = getdate(end_date)
-		return getdate() > getdate(end_date)
+		return getdate(frappe.flags.current_date) > getdate(end_date)
 
-	def is_past_grace_period(self):
+	def get_status_for_past_grace_period(self) -> str:
+		cancel_after_grace = cint(frappe.get_value("Subscription Settings", None, "cancel_after_grace"))
+		status = "Unpaid"
+
+		if cancel_after_grace:
+			status = "Cancelled"
+
+		return status
+
+	def is_past_grace_period(self) -> bool:
 		"""
 		Returns `True` if the grace period for the `Subscription` has passed
 		"""
-		current_invoice = self.get_current_invoice()
-		if self.current_invoice_is_past_due(current_invoice):
-			subscription_settings = frappe.get_single("Subscription Settings")
-			grace_period = cint(subscription_settings.grace_period)
+		if not self.current_invoice_is_past_due():
+			return
 
-			return getdate() > add_days(current_invoice.due_date, grace_period)
+		grace_period = cint(frappe.get_value("Subscription Settings", None, "grace_period"))
+		return getdate(frappe.flags.current_date) >= getdate(
+			add_days(self.current_invoice.due_date, grace_period)
+		)
 
-	def current_invoice_is_past_due(self, current_invoice=None):
+	def current_invoice_is_past_due(self) -> bool:
 		"""
 		Returns `True` if the current generated invoice is overdue
 		"""
-		if not current_invoice:
-			current_invoice = self.get_current_invoice()
-
-		if not current_invoice or self.is_paid(current_invoice):
+		if not self.current_invoice or self.is_paid(self.current_invoice):
 			return False
-		else:
-			return getdate() > getdate(current_invoice.due_date)
 
-	def get_current_invoice(self):
-		"""
-		Returns the most recent generated invoice.
-		"""
-		doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
+		return getdate(frappe.flags.current_date) >= getdate(self.current_invoice.due_date)
 
-		if len(self.invoices):
-			current = self.invoices[-1]
-			if frappe.db.exists(doctype, current.get("invoice")):
-				doc = frappe.get_doc(doctype, current.get("invoice"))
-				return doc
-			else:
-				frappe.throw(_("Invoice {0} no longer exists").format(current.get("invoice")))
+	@property
+	def invoice_document_type(self) -> str:
+		return "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
 
-	def is_new_subscription(self):
+	def is_new_subscription(self) -> bool:
 		"""
 		Returns `True` if `Subscription` has never generated an invoice
 		"""
-		return len(self.invoices) == 0
+		return self.is_new() or not frappe.db.exists(
+			{"doctype": self.invoice_document_type, "subscription": self.name}
+		)
 
-	def validate(self):
+	def validate(self) -> None:
 		self.validate_trial_period()
 		self.validate_plans_billing_cycle(self.get_billing_cycle_and_interval())
 		self.validate_end_date()
 		self.validate_to_follow_calendar_months()
 		if not self.cost_center:
-			self.cost_center = erpnext.get_default_cost_center(self.get("company"))
+			self.cost_center = get_default_cost_center(self.get("company"))
 
-	def validate_trial_period(self):
+	def validate_trial_period(self) -> None:
 		"""
 		Runs sanity checks on trial period dates for the `Subscription`
 		"""
@@ -297,7 +284,7 @@
 		if self.trial_period_start and getdate(self.trial_period_start) > getdate(self.start_date):
 			frappe.throw(_("Trial Period Start date cannot be after Subscription Start Date"))
 
-	def validate_end_date(self):
+	def validate_end_date(self) -> None:
 		billing_cycle_info = self.get_billing_cycle_data()
 		end_date = add_to_date(self.start_date, **billing_cycle_info)
 
@@ -306,53 +293,53 @@
 				_("Subscription End Date must be after {0} as per the subscription plan").format(end_date)
 			)
 
-	def validate_to_follow_calendar_months(self):
-		if self.follow_calendar_months:
-			billing_info = self.get_billing_cycle_and_interval()
+	def validate_to_follow_calendar_months(self) -> None:
+		if not self.follow_calendar_months:
+			return
 
-			if not self.end_date:
-				frappe.throw(_("Subscription End Date is mandatory to follow calendar months"))
+		billing_info = self.get_billing_cycle_and_interval()
 
-			if billing_info[0]["billing_interval"] != "Month":
-				frappe.throw(
-					_("Billing Interval in Subscription Plan must be Month to follow calendar months")
-				)
+		if not self.end_date:
+			frappe.throw(_("Subscription End Date is mandatory to follow calendar months"))
 
-	def after_insert(self):
+		if billing_info[0]["billing_interval"] != "Month":
+			frappe.throw(_("Billing Interval in Subscription Plan must be Month to follow calendar months"))
+
+	def after_insert(self) -> None:
 		# todo: deal with users who collect prepayments. Maybe a new Subscription Invoice doctype?
 		self.set_subscription_status()
 
-	def generate_invoice(self, prorate=0):
+	def generate_invoice(
+		self,
+		from_date: Optional[Union[str, datetime.date]] = None,
+		to_date: Optional[Union[str, datetime.date]] = None,
+	) -> Document:
 		"""
 		Creates a `Invoice` for the `Subscription`, updates `self.invoices` and
 		saves the `Subscription`.
+		Backwards compatibility
 		"""
+		return self.create_invoice(from_date=from_date, to_date=to_date)
 
-		doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
-
-		invoice = self.create_invoice(prorate)
-		self.append("invoices", {"document_type": doctype, "invoice": invoice.name})
-
-		self.save()
-
-		return invoice
-
-	def create_invoice(self, prorate):
+	def create_invoice(
+		self,
+		from_date: Optional[Union[str, datetime.date]] = None,
+		to_date: Optional[Union[str, datetime.date]] = None,
+	) -> Document:
 		"""
 		Creates a `Invoice`, submits it and returns it
 		"""
-		doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
-
-		invoice = frappe.new_doc(doctype)
-
 		# For backward compatibility
 		# Earlier subscription didn't had any company field
 		company = self.get("company") or get_default_company()
 		if not company:
+			# fmt: off
 			frappe.throw(
-				_("Company is mandatory was generating invoice. Please set default company in Global Defaults")
+				_("Company is mandatory was generating invoice. Please set default company in Global Defaults.")
 			)
+			# fmt: on
 
+		invoice = frappe.new_doc(self.invoice_document_type)
 		invoice.company = company
 		invoice.set_posting_time = 1
 		invoice.posting_date = (
@@ -363,17 +350,17 @@
 
 		invoice.cost_center = self.cost_center
 
-		if doctype == "Sales Invoice":
+		if self.invoice_document_type == "Sales Invoice":
 			invoice.customer = self.party
 		else:
 			invoice.supplier = self.party
 			if frappe.db.get_value("Supplier", self.party, "tax_withholding_category"):
 				invoice.apply_tds = 1
 
-		### Add party currency to invoice
+		# Add party currency to invoice
 		invoice.currency = get_party_account_currency(self.party_type, self.party, self.company)
 
-		## Add dimensions in invoice for subscription:
+		# Add dimensions in invoice for subscription:
 		accounting_dimensions = get_accounting_dimensions()
 
 		for dimension in accounting_dimensions:
@@ -382,7 +369,7 @@
 
 		# Subscription is better suited for service items. I won't update `update_stock`
 		# for that reason
-		items_list = self.get_items_from_plans(self.plans, prorate)
+		items_list = self.get_items_from_plans(self.plans, is_prorate())
 		for item in items_list:
 			item["cost_center"] = self.cost_center
 			invoice.append("items", item)
@@ -390,9 +377,9 @@
 		# Taxes
 		tax_template = ""
 
-		if doctype == "Sales Invoice" and self.sales_tax_template:
+		if self.invoice_document_type == "Sales Invoice" and self.sales_tax_template:
 			tax_template = self.sales_tax_template
-		if doctype == "Purchase Invoice" and self.purchase_tax_template:
+		if self.invoice_document_type == "Purchase Invoice" and self.purchase_tax_template:
 			tax_template = self.purchase_tax_template
 
 		if tax_template:
@@ -424,8 +411,9 @@
 				invoice.apply_discount_on = discount_on if discount_on else "Grand Total"
 
 		# Subscription period
-		invoice.from_date = self.current_invoice_start
-		invoice.to_date = self.current_invoice_end
+		invoice.subscription = self.name
+		invoice.from_date = from_date or self.current_invoice_start
+		invoice.to_date = to_date or self.current_invoice_end
 
 		invoice.flags.ignore_mandatory = True
 
@@ -437,13 +425,20 @@
 
 		return invoice
 
-	def get_items_from_plans(self, plans, prorate=0):
+	def get_items_from_plans(
+		self, plans: List[Dict[str, str]], prorate: Optional[bool] = None
+	) -> List[Dict]:
 		"""
 		Returns the `Item`s linked to `Subscription Plan`
 		"""
+		if prorate is None:
+			prorate = False
+
 		if prorate:
 			prorate_factor = get_prorata_factor(
-				self.current_invoice_end, self.current_invoice_start, self.generate_invoice_at_period_start
+				self.current_invoice_end,
+				self.current_invoice_start,
+				cint(self.generate_invoice_at_period_start),
 			)
 
 		items = []
@@ -465,7 +460,11 @@
 					"item_code": item_code,
 					"qty": plan.qty,
 					"rate": get_plan_rate(
-						plan.plan, plan.qty, party, self.current_invoice_start, self.current_invoice_end
+						plan.plan,
+						plan.qty,
+						party,
+						self.current_invoice_start,
+						self.current_invoice_end,
 					),
 					"cost_center": plan_doc.cost_center,
 				}
@@ -503,254 +502,184 @@
 
 		return items
 
-	def process(self):
+	@frappe.whitelist()
+	def process(self) -> bool:
 		"""
 		To be called by task periodically. It checks the subscription and takes appropriate action
 		as need be. It calls either of these methods depending the `Subscription` status:
 		1. `process_for_active`
 		2. `process_for_past_due`
 		"""
-		if self.status == "Active":
-			self.process_for_active()
-		elif self.status in ["Past Due Date", "Unpaid"]:
-			self.process_for_past_due_date()
+		if (
+			not self.is_current_invoice_generated(self.current_invoice_start, self.current_invoice_end)
+			and self.can_generate_new_invoice()
+		):
+			self.generate_invoice()
+			self.update_subscription_period(add_days(self.current_invoice_end, 1))
+
+		if self.cancel_at_period_end and (
+			getdate(frappe.flags.current_date) >= getdate(self.current_invoice_end)
+			or getdate(frappe.flags.current_date) >= getdate(self.end_date)
+		):
+			self.cancel_subscription()
 
 		self.set_subscription_status()
 
 		self.save()
 
-	def is_postpaid_to_invoice(self):
-		return getdate() > getdate(self.current_invoice_end) or (
-			getdate() >= getdate(self.current_invoice_end)
-			and getdate(self.current_invoice_end) == getdate(self.current_invoice_start)
-		)
+	def can_generate_new_invoice(self) -> bool:
+		if self.cancelation_date:
+			return False
+		elif self.generate_invoice_at_period_start and (
+			getdate(frappe.flags.current_date) == getdate(self.current_invoice_start)
+			or self.is_new_subscription()
+		):
+			return True
+		elif getdate(frappe.flags.current_date) == getdate(self.current_invoice_end):
+			if self.has_outstanding_invoice() and not self.generate_new_invoices_past_due_date:
+				return False
 
-	def is_prepaid_to_invoice(self):
-		if not self.generate_invoice_at_period_start:
+			return True
+		else:
 			return False
 
-		if self.is_new_subscription() and getdate() >= getdate(self.current_invoice_start):
-			return True
-
-		# Check invoice dates and make sure it doesn't have outstanding invoices
-		return getdate() >= getdate(self.current_invoice_start)
-
-	def is_current_invoice_generated(self, _current_start_date=None, _current_end_date=None):
-		invoice = self.get_current_invoice()
-
+	def is_current_invoice_generated(
+		self,
+		_current_start_date: Union[datetime.date, str] = None,
+		_current_end_date: Union[datetime.date, str] = None,
+	) -> bool:
 		if not (_current_start_date and _current_end_date):
-			_current_start_date, _current_end_date = self.update_subscription_period(
-				date=add_days(self.current_invoice_end, 1), return_date=True
+			_current_start_date, _current_end_date = self._get_subscription_period(
+				date=add_days(self.current_invoice_end, 1)
 			)
 
-		if invoice and getdate(_current_start_date) <= getdate(invoice.posting_date) <= getdate(
-			_current_end_date
-		):
+		if self.current_invoice and getdate(_current_start_date) <= getdate(
+			self.current_invoice.posting_date
+		) <= getdate(_current_end_date):
 			return True
 
 		return False
 
-	def process_for_active(self):
+	@property
+	def current_invoice(self) -> Union[Document, None]:
 		"""
-		Called by `process` if the status of the `Subscription` is 'Active'.
-
-		The possible outcomes of this method are:
-		1. Generate a new invoice
-		2. Change the `Subscription` status to 'Past Due Date'
-		3. Change the `Subscription` status to 'Cancelled'
+		Adds property for accessing the current_invoice
 		"""
+		return self.get_current_invoice()
 
-		if not self.is_current_invoice_generated(
-			self.current_invoice_start, self.current_invoice_end
-		) and (self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice()):
+	def get_current_invoice(self) -> Union[Document, None]:
+		"""
+		Returns the most recent generated invoice.
+		"""
+		invoice = frappe.get_all(
+			self.invoice_document_type,
+			{
+				"subscription": self.name,
+			},
+			limit=1,
+			order_by="to_date desc",
+			pluck="name",
+		)
 
-			prorate = frappe.db.get_single_value("Subscription Settings", "prorate")
-			self.generate_invoice(prorate)
+		if invoice:
+			return frappe.get_doc(self.invoice_document_type, invoice[0])
 
-		if getdate() > getdate(self.current_invoice_end) and self.is_prepaid_to_invoice():
-			self.update_subscription_period(add_days(self.current_invoice_end, 1))
-
-		if self.cancel_at_period_end and getdate() > getdate(self.current_invoice_end):
-			self.cancel_subscription_at_period_end()
-
-	def cancel_subscription_at_period_end(self):
+	def cancel_subscription_at_period_end(self) -> None:
 		"""
 		Called when `Subscription.cancel_at_period_end` is truthy
 		"""
-		if self.end_date and getdate() < getdate(self.end_date):
-			return
-
 		self.status = "Cancelled"
-		if not self.cancelation_date:
-			self.cancelation_date = nowdate()
+		self.cancelation_date = nowdate()
 
-	def process_for_past_due_date(self):
-		"""
-		Called by `process` if the status of the `Subscription` is 'Past Due Date'.
-
-		The possible outcomes of this method are:
-		1. Change the `Subscription` status to 'Active'
-		2. Change the `Subscription` status to 'Cancelled'
-		3. Change the `Subscription` status to 'Unpaid'
-		"""
-		current_invoice = self.get_current_invoice()
-		if not current_invoice:
-			frappe.throw(_("Current invoice {0} is missing").format(current_invoice.invoice))
-		else:
-			if not self.has_outstanding_invoice():
-				self.status = "Active"
-			else:
-				self.set_status_grace_period()
-
-			if getdate() > getdate(self.current_invoice_end):
-				self.update_subscription_period(add_days(self.current_invoice_end, 1))
-
-			# Generate invoices periodically even if current invoice are unpaid
-			if (
-				self.generate_new_invoices_past_due_date
-				and not self.is_current_invoice_generated(self.current_invoice_start, self.current_invoice_end)
-				and (self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice())
-			):
-
-				prorate = frappe.db.get_single_value("Subscription Settings", "prorate")
-				self.generate_invoice(prorate)
+	@property
+	def invoices(self) -> List[Dict]:
+		return frappe.get_all(
+			self.invoice_document_type,
+			filters={"subscription": self.name},
+			order_by="from_date asc",
+		)
 
 	@staticmethod
-	def is_paid(invoice):
+	def is_paid(invoice: Document) -> bool:
 		"""
 		Return `True` if the given invoice is paid
 		"""
 		return invoice.status == "Paid"
 
-	def has_outstanding_invoice(self):
+	def has_outstanding_invoice(self) -> int:
 		"""
 		Returns `True` if the most recent invoice for the `Subscription` is not paid
 		"""
-		doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
-		current_invoice = self.get_current_invoice()
-		invoice_list = [d.invoice for d in self.invoices]
-
-		outstanding_invoices = frappe.get_all(
-			doctype, fields=["name"], filters={"status": ("!=", "Paid"), "name": ("in", invoice_list)}
+		return frappe.db.count(
+			self.invoice_document_type,
+			{
+				"subscription": self.name,
+				"status": ["!=", "Paid"],
+			},
 		)
 
-		if outstanding_invoices:
-			return True
-		else:
-			False
-
-	def cancel_subscription(self):
+	@frappe.whitelist()
+	def cancel_subscription(self) -> None:
 		"""
 		This sets the subscription as cancelled. It will stop invoices from being generated
 		but it will not affect already created invoices.
 		"""
-		if self.status != "Cancelled":
-			to_generate_invoice = (
-				True if self.status == "Active" and not self.generate_invoice_at_period_start else False
-			)
-			to_prorate = frappe.db.get_single_value("Subscription Settings", "prorate")
-			self.status = "Cancelled"
-			self.cancelation_date = nowdate()
-			if to_generate_invoice:
-				self.generate_invoice(prorate=to_prorate)
-			self.save()
+		if self.status == "Cancelled":
+			frappe.throw(_("subscription is already cancelled."), InvoiceCancelled)
 
-	def restart_subscription(self):
+		to_generate_invoice = (
+			True if self.status == "Active" and not self.generate_invoice_at_period_start else False
+		)
+		self.status = "Cancelled"
+		self.cancelation_date = nowdate()
+
+		if to_generate_invoice:
+			self.generate_invoice(self.current_invoice_start, self.cancelation_date)
+
+		self.save()
+
+	@frappe.whitelist()
+	def restart_subscription(self) -> None:
 		"""
 		This sets the subscription as active. The subscription will be made to be like a new
 		subscription and the `Subscription` will lose all the history of generated invoices
 		it has.
 		"""
-		if self.status == "Cancelled":
-			self.status = "Active"
-			self.db_set("start_date", nowdate())
-			self.update_subscription_period(nowdate())
-			self.invoices = []
-			self.save()
-		else:
-			frappe.throw(_("You cannot restart a Subscription that is not cancelled."))
+		if not self.status == "Cancelled":
+			frappe.throw(_("You cannot restart a Subscription that is not cancelled."), InvoiceNotCancelled)
 
-	def get_precision(self):
-		invoice = self.get_current_invoice()
-		if invoice:
-			return invoice.precision("grand_total")
+		self.status = "Active"
+		self.cancelation_date = None
+		self.update_subscription_period(frappe.flags.current_date or nowdate())
+		self.save()
 
 
-def get_calendar_months(billing_interval):
-	calendar_months = []
-	start = 0
-	while start < 12:
-		start += billing_interval
-		calendar_months.append(start)
-
-	return calendar_months
+def is_prorate() -> int:
+	return cint(frappe.db.get_single_value("Subscription Settings", "prorate"))
 
 
-def get_prorata_factor(period_end, period_start, is_prepaid):
+def get_prorata_factor(
+	period_end: Union[datetime.date, str],
+	period_start: Union[datetime.date, str],
+	is_prepaid: Optional[int] = None,
+) -> Union[int, float]:
 	if is_prepaid:
-		prorate_factor = 1
-	else:
-		diff = flt(date_diff(nowdate(), period_start) + 1)
-		plan_days = flt(date_diff(period_end, period_start) + 1)
-		prorate_factor = diff / plan_days
+		return 1
 
-	return prorate_factor
+	diff = flt(date_diff(nowdate(), period_start) + 1)
+	plan_days = flt(date_diff(period_end, period_start) + 1)
+	return diff / plan_days
 
 
-def process_all():
+def process_all() -> None:
 	"""
 	Task to updates the status of all `Subscription` apart from those that are cancelled
 	"""
-	subscriptions = get_all_subscriptions()
-	for subscription in subscriptions:
-		process(subscription)
-
-
-def get_all_subscriptions():
-	"""
-	Returns all `Subscription` documents
-	"""
-	return frappe.db.get_all("Subscription", {"status": ("!=", "Cancelled")})
-
-
-def process(data):
-	"""
-	Checks a `Subscription` and updates it status as necessary
-	"""
-	if data:
+	for subscription in frappe.get_all("Subscription", {"status": ("!=", "Cancelled")}, pluck="name"):
 		try:
-			subscription = frappe.get_doc("Subscription", data["name"])
+			subscription = frappe.get_doc("Subscription", subscription)
 			subscription.process()
 			frappe.db.commit()
 		except frappe.ValidationError:
 			frappe.db.rollback()
 			subscription.log_error("Subscription failed")
-
-
-@frappe.whitelist()
-def cancel_subscription(name):
-	"""
-	Cancels a `Subscription`. This will stop the `Subscription` from further invoicing the
-	`Subscriber` but all already outstanding invoices will not be affected.
-	"""
-	subscription = frappe.get_doc("Subscription", name)
-	subscription.cancel_subscription()
-
-
-@frappe.whitelist()
-def restart_subscription(name):
-	"""
-	Restarts a cancelled `Subscription`. The `Subscription` will 'forget' the history of
-	all invoices it has generated
-	"""
-	subscription = frappe.get_doc("Subscription", name)
-	subscription.restart_subscription()
-
-
-@frappe.whitelist()
-def get_subscription_updates(name):
-	"""
-	Use this to get the latest state of the given `Subscription`
-	"""
-	subscription = frappe.get_doc("Subscription", name)
-	subscription.process()
diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py
index eb17daa..0bb171f 100644
--- a/erpnext/accounts/doctype/subscription/test_subscription.py
+++ b/erpnext/accounts/doctype/subscription/test_subscription.py
@@ -11,6 +11,7 @@
 	date_diff,
 	flt,
 	get_date_str,
+	getdate,
 	nowdate,
 )
 
@@ -90,10 +91,18 @@
 		customer.insert()
 
 
+def reset_settings():
+	settings = frappe.get_single("Subscription Settings")
+	settings.grace_period = 0
+	settings.cancel_after_grace = 0
+	settings.save()
+
+
 class TestSubscription(unittest.TestCase):
 	def setUp(self):
 		create_plan()
 		create_parties()
+		reset_settings()
 
 	def test_create_subscription_with_trial_with_correct_period(self):
 		subscription = frappe.new_doc("Subscription")
@@ -116,8 +125,6 @@
 		self.assertEqual(subscription.invoices, [])
 		self.assertEqual(subscription.status, "Trialling")
 
-		subscription.delete()
-
 	def test_create_subscription_without_trial_with_correct_period(self):
 		subscription = frappe.new_doc("Subscription")
 		subscription.party_type = "Customer"
@@ -133,8 +140,6 @@
 		self.assertEqual(len(subscription.invoices), 0)
 		self.assertEqual(subscription.status, "Active")
 
-		subscription.delete()
-
 	def test_create_subscription_trial_with_wrong_dates(self):
 		subscription = frappe.new_doc("Subscription")
 		subscription.party_type = "Customer"
@@ -144,7 +149,6 @@
 		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
 
 		self.assertRaises(frappe.ValidationError, subscription.save)
-		subscription.delete()
 
 	def test_create_subscription_multi_with_different_billing_fails(self):
 		subscription = frappe.new_doc("Subscription")
@@ -156,7 +160,6 @@
 		subscription.append("plans", {"plan": "_Test Plan Name 3", "qty": 1})
 
 		self.assertRaises(frappe.ValidationError, subscription.save)
-		subscription.delete()
 
 	def test_invoice_is_generated_at_end_of_billing_period(self):
 		subscription = frappe.new_doc("Subscription")
@@ -169,13 +172,13 @@
 		self.assertEqual(subscription.status, "Active")
 		self.assertEqual(subscription.current_invoice_start, "2018-01-01")
 		self.assertEqual(subscription.current_invoice_end, "2018-01-31")
+		frappe.flags.current_date = "2018-01-31"
 		subscription.process()
 
 		self.assertEqual(len(subscription.invoices), 1)
-		self.assertEqual(subscription.current_invoice_start, "2018-01-01")
-		subscription.process()
+		self.assertEqual(subscription.current_invoice_start, "2018-02-01")
+		self.assertEqual(subscription.current_invoice_end, "2018-02-28")
 		self.assertEqual(subscription.status, "Unpaid")
-		subscription.delete()
 
 	def test_status_goes_back_to_active_after_invoice_is_paid(self):
 		subscription = frappe.new_doc("Subscription")
@@ -183,7 +186,9 @@
 		subscription.party = "_Test Customer"
 		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
 		subscription.start_date = "2018-01-01"
+		subscription.generate_invoice_at_period_start = True
 		subscription.insert()
+		frappe.flags.current_date = "2018-01-01"
 		subscription.process()  # generate first invoice
 		self.assertEqual(len(subscription.invoices), 1)
 
@@ -203,11 +208,8 @@
 		self.assertEqual(subscription.current_invoice_start, add_months(subscription.start_date, 1))
 		self.assertEqual(len(subscription.invoices), 1)
 
-		subscription.delete()
-
 	def test_subscription_cancel_after_grace_period(self):
 		settings = frappe.get_single("Subscription Settings")
-		default_grace_period_action = settings.cancel_after_grace
 		settings.cancel_after_grace = 1
 		settings.save()
 
@@ -215,20 +217,18 @@
 		subscription.party_type = "Customer"
 		subscription.party = "_Test Customer"
 		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
+		# subscription.generate_invoice_at_period_start = True
 		subscription.start_date = "2018-01-01"
 		subscription.insert()
 
 		self.assertEqual(subscription.status, "Active")
 
+		frappe.flags.current_date = "2018-01-31"
 		subscription.process()  # generate first invoice
 		# This should change status to Cancelled since grace period is 0
 		# And is backdated subscription so subscription will be cancelled after processing
 		self.assertEqual(subscription.status, "Cancelled")
 
-		settings.cancel_after_grace = default_grace_period_action
-		settings.save()
-		subscription.delete()
-
 	def test_subscription_unpaid_after_grace_period(self):
 		settings = frappe.get_single("Subscription Settings")
 		default_grace_period_action = settings.cancel_after_grace
@@ -248,21 +248,26 @@
 
 		settings.cancel_after_grace = default_grace_period_action
 		settings.save()
-		subscription.delete()
 
 	def test_subscription_invoice_days_until_due(self):
+		_date = add_months(nowdate(), -1)
 		subscription = frappe.new_doc("Subscription")
 		subscription.party_type = "Customer"
 		subscription.party = "_Test Customer"
-		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
 		subscription.days_until_due = 10
-		subscription.start_date = add_months(nowdate(), -1)
+		subscription.start_date = _date
+		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
 		subscription.insert()
+
+		frappe.flags.current_date = subscription.current_invoice_end
+
 		subscription.process()  # generate first invoice
 		self.assertEqual(len(subscription.invoices), 1)
 		self.assertEqual(subscription.status, "Active")
 
-		subscription.delete()
+		frappe.flags.current_date = add_days(subscription.current_invoice_end, 3)
+		self.assertEqual(len(subscription.invoices), 1)
+		self.assertEqual(subscription.status, "Active")
 
 	def test_subscription_is_past_due_doesnt_change_within_grace_period(self):
 		settings = frappe.get_single("Subscription Settings")
@@ -276,6 +281,8 @@
 		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
 		subscription.start_date = add_days(nowdate(), -1000)
 		subscription.insert()
+
+		frappe.flags.current_date = subscription.current_invoice_end
 		subscription.process()  # generate first invoice
 
 		self.assertEqual(subscription.status, "Past Due Date")
@@ -292,7 +299,6 @@
 
 		settings.grace_period = grace_period
 		settings.save()
-		subscription.delete()
 
 	def test_subscription_remains_active_during_invoice_period(self):
 		subscription = frappe.new_doc("Subscription")
@@ -319,8 +325,6 @@
 		self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1))
 		self.assertEqual(len(subscription.invoices), 0)
 
-		subscription.delete()
-
 	def test_subscription_cancelation(self):
 		subscription = frappe.new_doc("Subscription")
 		subscription.party_type = "Customer"
@@ -331,8 +335,6 @@
 
 		self.assertEqual(subscription.status, "Cancelled")
 
-		subscription.delete()
-
 	def test_subscription_cancellation_invoices(self):
 		settings = frappe.get_single("Subscription Settings")
 		to_prorate = settings.prorate
@@ -372,7 +374,6 @@
 		self.assertEqual(flt(invoice.grand_total, 2), flt(prorate_factor * 900, 2))
 		self.assertEqual(subscription.status, "Cancelled")
 
-		subscription.delete()
 		settings.prorate = to_prorate
 		settings.save()
 
@@ -395,8 +396,6 @@
 		settings.prorate = to_prorate
 		settings.save()
 
-		subscription.delete()
-
 	def test_subscription_cancellation_invoices_with_prorata_true(self):
 		settings = frappe.get_single("Subscription Settings")
 		to_prorate = settings.prorate
@@ -422,8 +421,6 @@
 		settings.prorate = to_prorate
 		settings.save()
 
-		subscription.delete()
-
 	def test_subcription_cancellation_and_process(self):
 		settings = frappe.get_single("Subscription Settings")
 		default_grace_period_action = settings.cancel_after_grace
@@ -437,23 +434,22 @@
 		subscription.start_date = "2018-01-01"
 		subscription.insert()
 		subscription.process()  # generate first invoice
-		invoices = len(subscription.invoices)
 
+		# Generate an invoice for the cancelled period
 		subscription.cancel_subscription()
 		self.assertEqual(subscription.status, "Cancelled")
-		self.assertEqual(len(subscription.invoices), invoices)
+		self.assertEqual(len(subscription.invoices), 1)
 
 		subscription.process()
 		self.assertEqual(subscription.status, "Cancelled")
-		self.assertEqual(len(subscription.invoices), invoices)
+		self.assertEqual(len(subscription.invoices), 1)
 
 		subscription.process()
 		self.assertEqual(subscription.status, "Cancelled")
-		self.assertEqual(len(subscription.invoices), invoices)
+		self.assertEqual(len(subscription.invoices), 1)
 
 		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")
@@ -468,6 +464,7 @@
 		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
 		subscription.start_date = "2018-01-01"
 		subscription.insert()
+		frappe.flags.current_date = "2018-01-31"
 		subscription.process()  # generate first invoice
 
 		# Status is unpaid as Days until Due is zero and grace period is Zero
@@ -478,19 +475,18 @@
 
 		subscription.restart_subscription()
 		self.assertEqual(subscription.status, "Active")
-		self.assertEqual(len(subscription.invoices), 0)
+		self.assertEqual(len(subscription.invoices), 1)
 
 		subscription.process()
-		self.assertEqual(subscription.status, "Active")
-		self.assertEqual(len(subscription.invoices), 0)
+		self.assertEqual(subscription.status, "Unpaid")
+		self.assertEqual(len(subscription.invoices), 1)
 
 		subscription.process()
-		self.assertEqual(subscription.status, "Active")
-		self.assertEqual(len(subscription.invoices), 0)
+		self.assertEqual(subscription.status, "Unpaid")
+		self.assertEqual(len(subscription.invoices), 1)
 
 		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")
@@ -503,8 +499,11 @@
 		subscription.party = "_Test Customer"
 		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
 		subscription.start_date = "2018-01-01"
+		subscription.generate_invoice_at_period_start = True
 		subscription.insert()
 
+		frappe.flags.current_date = subscription.current_invoice_start
+
 		subscription.process()  # generate first invoice
 		# This should change status to Unpaid since grace period is 0
 		self.assertEqual(subscription.status, "Unpaid")
@@ -517,12 +516,12 @@
 		self.assertEqual(subscription.status, "Active")
 
 		# A new invoice is generated
+		frappe.flags.current_date = subscription.current_invoice_start
 		subscription.process()
 		self.assertEqual(subscription.status, "Unpaid")
 
 		settings.cancel_after_grace = default_grace_period_action
 		settings.save()
-		subscription.delete()
 
 	def test_restart_active_subscription(self):
 		subscription = frappe.new_doc("Subscription")
@@ -533,8 +532,6 @@
 
 		self.assertRaises(frappe.ValidationError, subscription.restart_subscription)
 
-		subscription.delete()
-
 	def test_subscription_invoice_discount_percentage(self):
 		subscription = frappe.new_doc("Subscription")
 		subscription.party_type = "Customer"
@@ -549,8 +546,6 @@
 		self.assertEqual(invoice.additional_discount_percentage, 10)
 		self.assertEqual(invoice.apply_discount_on, "Grand Total")
 
-		subscription.delete()
-
 	def test_subscription_invoice_discount_amount(self):
 		subscription = frappe.new_doc("Subscription")
 		subscription.party_type = "Customer"
@@ -565,8 +560,6 @@
 		self.assertEqual(invoice.discount_amount, 11)
 		self.assertEqual(invoice.apply_discount_on, "Grand Total")
 
-		subscription.delete()
-
 	def test_prepaid_subscriptions(self):
 		# Create a non pre-billed subscription, processing should not create
 		# invoices.
@@ -614,8 +607,6 @@
 		settings.prorate = to_prorate
 		settings.save()
 
-		subscription.delete()
-
 	def test_subscription_with_follow_calendar_months(self):
 		subscription = frappe.new_doc("Subscription")
 		subscription.party_type = "Supplier"
@@ -623,14 +614,14 @@
 		subscription.generate_invoice_at_period_start = 1
 		subscription.follow_calendar_months = 1
 
-		# select subscription start date as '2018-01-15'
+		# select subscription start date as "2018-01-15"
 		subscription.start_date = "2018-01-15"
 		subscription.end_date = "2018-07-15"
 		subscription.append("plans", {"plan": "_Test Plan Name 4", "qty": 1})
 		subscription.save()
 
-		# even though subscription starts at '2018-01-15' and Billing interval is Month and count 3
-		# First invoice will end at '2018-03-31' instead of '2018-04-14'
+		# even though subscription starts at "2018-01-15" and Billing interval is Month and count 3
+		# First invoice will end at "2018-03-31" instead of "2018-04-14"
 		self.assertEqual(get_date_str(subscription.current_invoice_end), "2018-03-31")
 
 	def test_subscription_generate_invoice_past_due(self):
@@ -639,11 +630,12 @@
 		subscription.party = "_Test Supplier"
 		subscription.generate_invoice_at_period_start = 1
 		subscription.generate_new_invoices_past_due_date = 1
-		# select subscription start date as '2018-01-15'
+		# select subscription start date as "2018-01-15"
 		subscription.start_date = "2018-01-01"
 		subscription.append("plans", {"plan": "_Test Plan Name 4", "qty": 1})
 		subscription.save()
 
+		frappe.flags.current_date = "2018-01-01"
 		# Process subscription and create first invoice
 		# Subscription status will be unpaid since due date has already passed
 		subscription.process()
@@ -652,8 +644,8 @@
 
 		# Now the Subscription is unpaid
 		# Even then new invoice should be created as we have enabled `generate_new_invoices_past_due_date` in
-		# subscription
-
+		# subscription and the interval between the subscriptions is 3 months
+		frappe.flags.current_date = "2018-04-01"
 		subscription.process()
 		self.assertEqual(len(subscription.invoices), 2)
 
@@ -662,7 +654,7 @@
 		subscription.party_type = "Supplier"
 		subscription.party = "_Test Supplier"
 		subscription.generate_invoice_at_period_start = 1
-		# select subscription start date as '2018-01-15'
+		# select subscription start date as "2018-01-15"
 		subscription.start_date = "2018-01-01"
 		subscription.append("plans", {"plan": "_Test Plan Name 4", "qty": 1})
 		subscription.save()
@@ -682,7 +674,7 @@
 		subscription.party = "_Test Subscription Customer"
 		subscription.generate_invoice_at_period_start = 1
 		subscription.company = "_Test Company"
-		# select subscription start date as '2018-01-15'
+		# select subscription start date as "2018-01-15"
 		subscription.start_date = "2018-01-01"
 		subscription.append("plans", {"plan": "_Test Plan Multicurrency", "qty": 1})
 		subscription.save()
@@ -692,5 +684,47 @@
 		self.assertEqual(subscription.status, "Unpaid")
 
 		# Check the currency of the created invoice
-		currency = frappe.db.get_value("Sales Invoice", subscription.invoices[0].invoice, "currency")
+		currency = frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "currency")
 		self.assertEqual(currency, "USD")
+
+	def test_subscription_recovery(self):
+		"""Test if Subscription recovers when start/end date run out of sync with created invoices."""
+		subscription = frappe.new_doc("Subscription")
+		subscription.party_type = "Customer"
+		subscription.party = "_Test Subscription Customer"
+		subscription.company = "_Test Company"
+		subscription.start_date = "2021-12-01"
+		subscription.generate_new_invoices_past_due_date = 1
+		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
+		subscription.submit_invoice = 0
+		subscription.save()
+
+		# create invoices for the first two moths
+		frappe.flags.current_date = "2021-12-31"
+		subscription.process()
+
+		frappe.flags.current_date = "2022-01-31"
+		subscription.process()
+
+		self.assertEqual(len(subscription.invoices), 2)
+		self.assertEqual(
+			getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "from_date")),
+			getdate("2021-12-01"),
+		)
+		self.assertEqual(
+			getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[1].name, "from_date")),
+			getdate("2022-01-01"),
+		)
+
+		# recreate most recent invoice
+		subscription.process()
+
+		self.assertEqual(len(subscription.invoices), 2)
+		self.assertEqual(
+			getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "from_date")),
+			getdate("2021-12-01"),
+		)
+		self.assertEqual(
+			getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[1].name, "from_date")),
+			getdate("2022-01-01"),
+		)
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 641d755..d53bace 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -262,6 +262,7 @@
 erpnext.patches.v15_0.saudi_depreciation_warning
 erpnext.patches.v15_0.delete_saudi_doctypes
 erpnext.patches.v14_0.show_loan_management_deprecation_warning
+erpnext.patches.v14_0.update_subscription_details
 execute:frappe.rename_doc("Report", "TDS Payable Monthly", "Tax Withholding Details", force=True)
 
 [post_model_sync]
diff --git a/erpnext/patches/v14_0/update_subscription_details.py b/erpnext/patches/v14_0/update_subscription_details.py
new file mode 100644
index 0000000..729ac18
--- /dev/null
+++ b/erpnext/patches/v14_0/update_subscription_details.py
@@ -0,0 +1,17 @@
+import frappe
+
+
+def execute():
+	subscription_invoices = frappe.get_all(
+		"Subscription Invoice", fields=["document_type", "invoice", "parent"]
+	)
+
+	for subscription_invoice in subscription_invoices:
+		frappe.db.set_value(
+			subscription_invoice.document_type,
+			subscription_invoice.invoice,
+			"Subscription",
+			subscription_invoice.parent,
+		)
+
+	frappe.delete_doc_if_exists("DocType", "Subscription Invoice")