feat: Add a process document for Subscription (#37126)

* feat: Add a process document for Subscription

* chore: Remove print statements

* feat: Input for generating invoice before currenc invoice date

* chore: patch for backward compatability

* refactor: Unit tests for subscription

* chore: set status on insert
diff --git a/erpnext/accounts/doctype/process_subscription/__init__.py b/erpnext/accounts/doctype/process_subscription/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/process_subscription/__init__.py
diff --git a/erpnext/accounts/doctype/process_subscription/process_subscription.js b/erpnext/accounts/doctype/process_subscription/process_subscription.js
new file mode 100644
index 0000000..858c913
--- /dev/null
+++ b/erpnext/accounts/doctype/process_subscription/process_subscription.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+// frappe.ui.form.on("Process Subscription", {
+// 	refresh(frm) {
+
+// 	},
+// });
diff --git a/erpnext/accounts/doctype/process_subscription/process_subscription.json b/erpnext/accounts/doctype/process_subscription/process_subscription.json
new file mode 100644
index 0000000..502d002
--- /dev/null
+++ b/erpnext/accounts/doctype/process_subscription/process_subscription.json
@@ -0,0 +1,90 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2023-09-17 15:40:59.724177",
+ "default_view": "List",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+  "posting_date",
+  "subscription",
+  "amended_from"
+ ],
+ "fields": [
+  {
+   "fieldname": "amended_from",
+   "fieldtype": "Link",
+   "label": "Amended From",
+   "no_copy": 1,
+   "options": "Process Subscription",
+   "print_hide": 1,
+   "read_only": 1
+  },
+  {
+   "fieldname": "posting_date",
+   "fieldtype": "Date",
+   "in_list_view": 1,
+   "label": "Posting Date",
+   "reqd": 1
+  },
+  {
+   "fieldname": "subscription",
+   "fieldtype": "Link",
+   "label": "Subscription",
+   "options": "Subscription"
+  }
+ ],
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2023-09-17 17:33:37.974166",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Process Subscription",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "System Manager",
+   "share": 1,
+   "submit": 1,
+   "write": 1
+  },
+  {
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "Accounts Manager",
+   "share": 1,
+   "submit": 1,
+   "write": 1
+  },
+  {
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "Accounts User",
+   "share": 1,
+   "submit": 1,
+   "write": 1
+  }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/process_subscription/process_subscription.py b/erpnext/accounts/doctype/process_subscription/process_subscription.py
new file mode 100644
index 0000000..99269d6
--- /dev/null
+++ b/erpnext/accounts/doctype/process_subscription/process_subscription.py
@@ -0,0 +1,27 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from datetime import datetime
+from typing import Union
+
+import frappe
+from frappe.model.document import Document
+from frappe.utils import getdate
+
+from erpnext.accounts.doctype.subscription.subscription import process_all
+
+
+class ProcessSubscription(Document):
+	def on_submit(self):
+		process_all(subscription=self.subscription, posting_date=self.posting_date)
+
+
+def create_subscription_process(
+	subscription: str | None, posting_date: Union[str, datetime.date] | None
+):
+	"""Create a new Process Subscription document"""
+	doc = frappe.new_doc("Process Subscription")
+	doc.subscription = subscription
+	doc.posting_date = getdate(posting_date)
+	doc.insert(ignore_permissions=True)
+	doc.submit()
diff --git a/erpnext/accounts/doctype/process_subscription/test_process_subscription.py b/erpnext/accounts/doctype/process_subscription/test_process_subscription.py
new file mode 100644
index 0000000..723695f
--- /dev/null
+++ b/erpnext/accounts/doctype/process_subscription/test_process_subscription.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestProcessSubscription(FrappeTestCase):
+	pass
diff --git a/erpnext/accounts/doctype/subscription/subscription.json b/erpnext/accounts/doctype/subscription/subscription.json
index c15aa1e..187b7ab 100644
--- a/erpnext/accounts/doctype/subscription/subscription.json
+++ b/erpnext/accounts/doctype/subscription/subscription.json
@@ -24,8 +24,9 @@
   "current_invoice_start",
   "current_invoice_end",
   "days_until_due",
+  "generate_invoice_at",
+  "number_of_days",
   "cancel_at_period_end",
-  "generate_invoice_at_period_start",
   "sb_4",
   "plans",
   "sb_1",
@@ -86,12 +87,14 @@
    "fieldname": "current_invoice_start",
    "fieldtype": "Date",
    "label": "Current Invoice Start Date",
+   "no_copy": 1,
    "read_only": 1
   },
   {
    "fieldname": "current_invoice_end",
    "fieldtype": "Date",
    "label": "Current Invoice End Date",
+   "no_copy": 1,
    "read_only": 1
   },
   {
@@ -108,12 +111,6 @@
    "label": "Cancel At End Of Period"
   },
   {
-   "default": "0",
-   "fieldname": "generate_invoice_at_period_start",
-   "fieldtype": "Check",
-   "label": "Generate Invoice At Beginning Of Period"
-  },
-  {
    "allow_on_submit": 1,
    "fieldname": "sb_4",
    "fieldtype": "Section Break",
@@ -240,6 +237,21 @@
    "fieldname": "submit_invoice",
    "fieldtype": "Check",
    "label": "Submit Generated Invoices"
+  },
+  {
+   "default": "End of the current subscription period",
+   "fieldname": "generate_invoice_at",
+   "fieldtype": "Select",
+   "label": "Generate Invoice At",
+   "options": "End of the current subscription period\nBeginning of the current subscription period\nDays before the current subscription period",
+   "reqd": 1
+  },
+  {
+   "depends_on": "eval:doc.generate_invoice_at === \"Days before the current subscription period\"",
+   "fieldname": "number_of_days",
+   "fieldtype": "Int",
+   "label": "Number of Days",
+   "mandatory_depends_on": "eval:doc.generate_invoice_at === \"Days before the current subscription period\""
   }
  ],
  "index_web_pages_for_search": 1,
@@ -255,7 +267,7 @@
    "link_fieldname": "subscription"
   }
  ],
- "modified": "2022-02-18 23:24:57.185054",
+ "modified": "2023-09-18 17:48:21.900252",
  "modified_by": "Administrator",
  "module": "Accounts",
  "name": "Subscription",
diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py
index bbcade1..3cf7d28 100644
--- a/erpnext/accounts/doctype/subscription/subscription.py
+++ b/erpnext/accounts/doctype/subscription/subscription.py
@@ -36,12 +36,15 @@
 	pass
 
 
+DateTimeLikeObject = Union[str, datetime.date]
+
+
 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: Optional[Union[datetime.date, str]] = None):
+	def update_subscription_period(self, date: Optional["DateTimeLikeObject"] = None):
 		"""
 		Subscription period is the period to be billed. This method updates the
 		beginning of the billing period and end of the billing period.
@@ -52,14 +55,14 @@
 		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):
+	def _get_subscription_period(self, date: Optional["DateTimeLikeObject"] = None):
 		_current_invoice_start = self.get_current_invoice_start(date)
 		_current_invoice_end = self.get_current_invoice_end(_current_invoice_start)
 
 		return _current_invoice_start, _current_invoice_end
 
 	def get_current_invoice_start(
-		self, date: Optional[Union[datetime.date, str]] = None
+		self, date: Optional["DateTimeLikeObject"] = None
 	) -> Union[datetime.date, str]:
 		"""
 		This returns the date of the beginning of the current billing period.
@@ -84,7 +87,7 @@
 		return _current_invoice_start
 
 	def get_current_invoice_end(
-		self, date: Optional[Union[datetime.date, str]] = None
+		self, date: Optional["DateTimeLikeObject"] = None
 	) -> Union[datetime.date, str]:
 		"""
 		This returns the date of the end of the current billing period.
@@ -179,30 +182,24 @@
 
 		return data
 
-	def set_subscription_status(self) -> None:
+	def set_subscription_status(self, posting_date: Optional["DateTimeLikeObject"] = None) -> None:
 		"""
 		Sets the status of the `Subscription`
 		"""
 		if self.is_trialling():
 			self.status = "Trialling"
 		elif (
-			self.status == "Active"
-			and self.end_date
-			and getdate(frappe.flags.current_date) > getdate(self.end_date)
+			self.status == "Active" 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()
-			self.cancelation_date = (
-				getdate(frappe.flags.current_date) if self.status == "Cancelled" else None
-			)
+			self.cancelation_date = getdate(posting_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() or self.is_new_subscription():
 			self.status = "Active"
 
-		self.save()
-
 	def is_trialling(self) -> bool:
 		"""
 		Returns `True` if the `Subscription` is in trial period.
@@ -210,7 +207,9 @@
 		return not self.period_has_passed(self.trial_period_end) and self.is_new_subscription()
 
 	@staticmethod
-	def period_has_passed(end_date: Union[str, datetime.date]) -> bool:
+	def period_has_passed(
+		end_date: Union[str, datetime.date], posting_date: Optional["DateTimeLikeObject"] = None
+	) -> bool:
 		"""
 		Returns true if the given `end_date` has passed
 		"""
@@ -218,7 +217,7 @@
 		if not end_date:
 			return True
 
-		return getdate(frappe.flags.current_date) > getdate(end_date)
+		return getdate(posting_date) > getdate(end_date)
 
 	def get_status_for_past_grace_period(self) -> str:
 		cancel_after_grace = cint(frappe.get_value("Subscription Settings", None, "cancel_after_grace"))
@@ -229,7 +228,7 @@
 
 		return status
 
-	def is_past_grace_period(self) -> bool:
+	def is_past_grace_period(self, posting_date: Optional["DateTimeLikeObject"] = None) -> bool:
 		"""
 		Returns `True` if the grace period for the `Subscription` has passed
 		"""
@@ -237,18 +236,18 @@
 			return
 
 		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)
-		)
+		return getdate(posting_date) >= getdate(add_days(self.current_invoice.due_date, grace_period))
 
-	def current_invoice_is_past_due(self) -> bool:
+	def current_invoice_is_past_due(
+		self, posting_date: Optional["DateTimeLikeObject"] = None
+	) -> bool:
 		"""
 		Returns `True` if the current generated invoice is overdue
 		"""
 		if not self.current_invoice or self.is_paid(self.current_invoice):
 			return False
 
-		return getdate(frappe.flags.current_date) >= getdate(self.current_invoice.due_date)
+		return getdate(posting_date) >= getdate(self.current_invoice.due_date)
 
 	@property
 	def invoice_document_type(self) -> str:
@@ -270,6 +269,9 @@
 		if not self.cost_center:
 			self.cost_center = get_default_cost_center(self.get("company"))
 
+		if self.is_new():
+			self.set_subscription_status()
+
 	def validate_trial_period(self) -> None:
 		"""
 		Runs sanity checks on trial period dates for the `Subscription`
@@ -305,10 +307,6 @@
 		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,
 		from_date: Optional[Union[str, datetime.date]] = None,
@@ -344,7 +342,7 @@
 		invoice.set_posting_time = 1
 		invoice.posting_date = (
 			self.current_invoice_start
-			if self.generate_invoice_at_period_start
+			if self.generate_invoice_at == "Beginning of the current subscription period"
 			else self.current_invoice_end
 		)
 
@@ -438,7 +436,7 @@
 			prorate_factor = get_prorata_factor(
 				self.current_invoice_end,
 				self.current_invoice_start,
-				cint(self.generate_invoice_at_period_start),
+				cint(self.generate_invoice_at == "Beginning of the current subscription period"),
 			)
 
 		items = []
@@ -503,42 +501,45 @@
 		return items
 
 	@frappe.whitelist()
-	def process(self) -> bool:
+	def process(self, posting_date: Optional["DateTimeLikeObject"] = None) -> 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 (
-			not self.is_current_invoice_generated(self.current_invoice_start, self.current_invoice_end)
-			and self.can_generate_new_invoice()
-		):
+		if not self.is_current_invoice_generated(
+			self.current_invoice_start, self.current_invoice_end
+		) and self.can_generate_new_invoice(posting_date):
 			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)
+			getdate(posting_date) >= getdate(self.current_invoice_end)
+			or getdate(posting_date) >= getdate(self.end_date)
 		):
 			self.cancel_subscription()
 
-		self.set_subscription_status()
+		self.set_subscription_status(posting_date=posting_date)
 
 		self.save()
 
-	def can_generate_new_invoice(self) -> bool:
+	def can_generate_new_invoice(self, posting_date: Optional["DateTimeLikeObject"] = None) -> 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()
+
+		if self.has_outstanding_invoice() and not self.generate_new_invoices_past_due_date:
+			return False
+
+		if self.generate_invoice_at == "Beginning of the current subscription period" and (
+			getdate(posting_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
-
+		elif self.generate_invoice_at == "Days before the current subscription period" and (
+			getdate(posting_date) == getdate(add_days(self.current_invoice_start, -1 * self.number_of_days))
+		):
+			return True
+		elif getdate(posting_date) == getdate(self.current_invoice_end):
 			return True
 		else:
 			return False
@@ -628,7 +629,10 @@
 			frappe.throw(_("subscription is already cancelled."), InvoiceCancelled)
 
 		to_generate_invoice = (
-			True if self.status == "Active" and not self.generate_invoice_at_period_start else False
+			True
+			if self.status == "Active"
+			and not self.generate_invoice_at == "Beginning of the current subscription period"
+			else False
 		)
 		self.status = "Cancelled"
 		self.cancelation_date = nowdate()
@@ -639,7 +643,7 @@
 		self.save()
 
 	@frappe.whitelist()
-	def restart_subscription(self) -> None:
+	def restart_subscription(self, posting_date: Optional["DateTimeLikeObject"] = None) -> 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
@@ -650,7 +654,7 @@
 
 		self.status = "Active"
 		self.cancelation_date = None
-		self.update_subscription_period(frappe.flags.current_date or nowdate())
+		self.update_subscription_period(posting_date or nowdate())
 		self.save()
 
 
@@ -671,14 +675,21 @@
 	return diff / plan_days
 
 
-def process_all() -> None:
+def process_all(
+	subscription: str | None, posting_date: Optional["DateTimeLikeObject"] = None
+) -> None:
 	"""
 	Task to updates the status of all `Subscription` apart from those that are cancelled
 	"""
-	for subscription in frappe.get_all("Subscription", {"status": ("!=", "Cancelled")}, pluck="name"):
+	filters = {"status": ("!=", "Cancelled")}
+
+	if subscription:
+		filters["name"] = subscription
+
+	for subscription in frappe.get_all("Subscription", filters, pluck="name"):
 		try:
 			subscription = frappe.get_doc("Subscription", subscription)
-			subscription.process()
+			subscription.process(posting_date)
 			frappe.db.commit()
 		except frappe.ValidationError:
 			frappe.db.rollback()
diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py
index 0bb171f..803e879 100644
--- a/erpnext/accounts/doctype/subscription/test_subscription.py
+++ b/erpnext/accounts/doctype/subscription/test_subscription.py
@@ -8,6 +8,7 @@
 	add_days,
 	add_months,
 	add_to_date,
+	cint,
 	date_diff,
 	flt,
 	get_date_str,
@@ -20,99 +21,16 @@
 test_dependencies = ("UOM", "Item Group", "Item")
 
 
-def create_plan():
-	if not frappe.db.exists("Subscription Plan", "_Test Plan Name"):
-		plan = frappe.new_doc("Subscription Plan")
-		plan.plan_name = "_Test Plan Name"
-		plan.item = "_Test Non Stock Item"
-		plan.price_determination = "Fixed Rate"
-		plan.cost = 900
-		plan.billing_interval = "Month"
-		plan.billing_interval_count = 1
-		plan.insert()
-
-	if not frappe.db.exists("Subscription Plan", "_Test Plan Name 2"):
-		plan = frappe.new_doc("Subscription Plan")
-		plan.plan_name = "_Test Plan Name 2"
-		plan.item = "_Test Non Stock Item"
-		plan.price_determination = "Fixed Rate"
-		plan.cost = 1999
-		plan.billing_interval = "Month"
-		plan.billing_interval_count = 1
-		plan.insert()
-
-	if not frappe.db.exists("Subscription Plan", "_Test Plan Name 3"):
-		plan = frappe.new_doc("Subscription Plan")
-		plan.plan_name = "_Test Plan Name 3"
-		plan.item = "_Test Non Stock Item"
-		plan.price_determination = "Fixed Rate"
-		plan.cost = 1999
-		plan.billing_interval = "Day"
-		plan.billing_interval_count = 14
-		plan.insert()
-
-	# Defined a quarterly Subscription Plan
-	if not frappe.db.exists("Subscription Plan", "_Test Plan Name 4"):
-		plan = frappe.new_doc("Subscription Plan")
-		plan.plan_name = "_Test Plan Name 4"
-		plan.item = "_Test Non Stock Item"
-		plan.price_determination = "Monthly Rate"
-		plan.cost = 20000
-		plan.billing_interval = "Month"
-		plan.billing_interval_count = 3
-		plan.insert()
-
-	if not frappe.db.exists("Subscription Plan", "_Test Plan Multicurrency"):
-		plan = frappe.new_doc("Subscription Plan")
-		plan.plan_name = "_Test Plan Multicurrency"
-		plan.item = "_Test Non Stock Item"
-		plan.price_determination = "Fixed Rate"
-		plan.cost = 50
-		plan.currency = "USD"
-		plan.billing_interval = "Month"
-		plan.billing_interval_count = 1
-		plan.insert()
-
-
-def create_parties():
-	if not frappe.db.exists("Supplier", "_Test Supplier"):
-		supplier = frappe.new_doc("Supplier")
-		supplier.supplier_name = "_Test Supplier"
-		supplier.supplier_group = "All Supplier Groups"
-		supplier.insert()
-
-	if not frappe.db.exists("Customer", "_Test Subscription Customer"):
-		customer = frappe.new_doc("Customer")
-		customer.customer_name = "_Test Subscription Customer"
-		customer.billing_currency = "USD"
-		customer.append(
-			"accounts", {"company": "_Test Company", "account": "_Test Receivable USD - _TC"}
-		)
-		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()
+		make_plans()
 		create_parties()
 		reset_settings()
 
 	def test_create_subscription_with_trial_with_correct_period(self):
-		subscription = frappe.new_doc("Subscription")
-		subscription.party_type = "Customer"
-		subscription.party = "_Test Customer"
-		subscription.trial_period_start = nowdate()
-		subscription.trial_period_end = add_months(nowdate(), 1)
-		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
-		subscription.save()
-
+		subscription = create_subscription(
+			trial_period_start=nowdate(), trial_period_end=add_months(nowdate(), 1)
+		)
 		self.assertEqual(subscription.trial_period_start, nowdate())
 		self.assertEqual(subscription.trial_period_end, add_months(nowdate(), 1))
 		self.assertEqual(
@@ -126,12 +44,7 @@
 		self.assertEqual(subscription.status, "Trialling")
 
 	def test_create_subscription_without_trial_with_correct_period(self):
-		subscription = frappe.new_doc("Subscription")
-		subscription.party_type = "Customer"
-		subscription.party = "_Test Customer"
-		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
-		subscription.save()
-
+		subscription = create_subscription()
 		self.assertEqual(subscription.trial_period_start, None)
 		self.assertEqual(subscription.trial_period_end, None)
 		self.assertEqual(subscription.current_invoice_start, nowdate())
@@ -141,55 +54,28 @@
 		self.assertEqual(subscription.status, "Active")
 
 	def test_create_subscription_trial_with_wrong_dates(self):
-		subscription = frappe.new_doc("Subscription")
-		subscription.party_type = "Customer"
-		subscription.party = "_Test Customer"
-		subscription.trial_period_end = nowdate()
-		subscription.trial_period_start = add_days(nowdate(), 30)
-		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
-
-		self.assertRaises(frappe.ValidationError, subscription.save)
-
-	def test_create_subscription_multi_with_different_billing_fails(self):
-		subscription = frappe.new_doc("Subscription")
-		subscription.party_type = "Customer"
-		subscription.party = "_Test Customer"
-		subscription.trial_period_end = nowdate()
-		subscription.trial_period_start = add_days(nowdate(), 30)
-		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
-		subscription.append("plans", {"plan": "_Test Plan Name 3", "qty": 1})
-
+		subscription = create_subscription(
+			trial_period_start=add_days(nowdate(), 30), trial_period_end=nowdate(), do_not_save=True
+		)
 		self.assertRaises(frappe.ValidationError, subscription.save)
 
 	def test_invoice_is_generated_at_end_of_billing_period(self):
-		subscription = frappe.new_doc("Subscription")
-		subscription.party_type = "Customer"
-		subscription.party = "_Test Customer"
-		subscription.start_date = "2018-01-01"
-		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
-		subscription.insert()
-
+		subscription = create_subscription(start_date="2018-01-01")
 		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()
 
+		subscription.process(posting_date="2018-01-31")
 		self.assertEqual(len(subscription.invoices), 1)
 		self.assertEqual(subscription.current_invoice_start, "2018-02-01")
 		self.assertEqual(subscription.current_invoice_end, "2018-02-28")
 		self.assertEqual(subscription.status, "Unpaid")
 
 	def test_status_goes_back_to_active_after_invoice_is_paid(self):
-		subscription = frappe.new_doc("Subscription")
-		subscription.party_type = "Customer"
-		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
+		subscription = create_subscription(
+			start_date="2018-01-01", generate_invoice_at="Beginning of the current subscription period"
+		)
+		subscription.process(posting_date="2018-01-01")  # generate first invoice
 		self.assertEqual(len(subscription.invoices), 1)
 
 		# Status is unpaid as Days until Due is zero and grace period is Zero
@@ -213,18 +99,10 @@
 		settings.cancel_after_grace = 1
 		settings.save()
 
-		subscription = frappe.new_doc("Subscription")
-		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()
-
+		subscription = create_subscription(start_date="2018-01-01")
 		self.assertEqual(subscription.status, "Active")
 
-		frappe.flags.current_date = "2018-01-31"
-		subscription.process()  # generate first invoice
+		subscription.process(posting_date="2018-01-31")  # 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")
@@ -235,13 +113,8 @@
 		settings.cancel_after_grace = 0
 		settings.save()
 
-		subscription = frappe.new_doc("Subscription")
-		subscription.party_type = "Customer"
-		subscription.party = "_Test Customer"
-		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
-		subscription.start_date = "2018-01-01"
-		subscription.insert()
-		subscription.process()  # generate first invoice
+		subscription = create_subscription(start_date="2018-01-01")
+		subscription.process(posting_date="2018-01-31")  # generate first invoice
 
 		# Status is unpaid as Days until Due is zero and grace period is Zero
 		self.assertEqual(subscription.status, "Unpaid")
@@ -251,21 +124,9 @@
 
 	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.days_until_due = 10
-		subscription.start_date = _date
-		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
-		subscription.insert()
+		subscription = create_subscription(start_date=_date, days_until_due=10)
 
-		frappe.flags.current_date = subscription.current_invoice_end
-
-		subscription.process()  # generate first invoice
-		self.assertEqual(len(subscription.invoices), 1)
-		self.assertEqual(subscription.status, "Active")
-
-		frappe.flags.current_date = add_days(subscription.current_invoice_end, 3)
+		subscription.process(posting_date=subscription.current_invoice_end)  # generate first invoice
 		self.assertEqual(len(subscription.invoices), 1)
 		self.assertEqual(subscription.status, "Active")
 
@@ -275,16 +136,9 @@
 		settings.grace_period = 1000
 		settings.save()
 
-		subscription = frappe.new_doc("Subscription")
-		subscription.party_type = "Customer"
-		subscription.party = "_Test Customer"
-		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
-		subscription.start_date = add_days(nowdate(), -1000)
-		subscription.insert()
+		subscription = create_subscription(start_date=add_days(nowdate(), -1000))
 
-		frappe.flags.current_date = subscription.current_invoice_end
-		subscription.process()  # generate first invoice
-
+		subscription.process(posting_date=subscription.current_invoice_end)  # generate first invoice
 		self.assertEqual(subscription.status, "Past Due Date")
 
 		subscription.process()
@@ -301,12 +155,7 @@
 		settings.save()
 
 	def test_subscription_remains_active_during_invoice_period(self):
-		subscription = frappe.new_doc("Subscription")
-		subscription.party_type = "Customer"
-		subscription.party = "_Test Customer"
-		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
-		subscription.save()
-		subscription.process()  # no changes expected
+		subscription = create_subscription()  # no changes expected
 
 		self.assertEqual(subscription.status, "Active")
 		self.assertEqual(subscription.current_invoice_start, nowdate())
@@ -325,12 +174,8 @@
 		self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1))
 		self.assertEqual(len(subscription.invoices), 0)
 
-	def test_subscription_cancelation(self):
-		subscription = frappe.new_doc("Subscription")
-		subscription.party_type = "Customer"
-		subscription.party = "_Test Customer"
-		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
-		subscription.save()
+	def test_subscription_cancellation(self):
+		subscription = create_subscription()
 		subscription.cancel_subscription()
 
 		self.assertEqual(subscription.status, "Cancelled")
@@ -341,11 +186,7 @@
 		settings.prorate = 1
 		settings.save()
 
-		subscription = frappe.new_doc("Subscription")
-		subscription.party_type = "Customer"
-		subscription.party = "_Test Customer"
-		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
-		subscription.save()
+		subscription = create_subscription()
 
 		self.assertEqual(subscription.status, "Active")
 
@@ -365,7 +206,7 @@
 				get_prorata_factor(
 					subscription.current_invoice_end,
 					subscription.current_invoice_start,
-					subscription.generate_invoice_at_period_start,
+					cint(subscription.generate_invoice_at == "Beginning of the current subscription period"),
 				),
 				2,
 			),
@@ -383,11 +224,7 @@
 		settings.prorate = 0
 		settings.save()
 
-		subscription = frappe.new_doc("Subscription")
-		subscription.party_type = "Customer"
-		subscription.party = "_Test Customer"
-		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
-		subscription.save()
+		subscription = create_subscription()
 		subscription.cancel_subscription()
 		invoice = subscription.get_current_invoice()
 
@@ -402,11 +239,7 @@
 		settings.prorate = 1
 		settings.save()
 
-		subscription = frappe.new_doc("Subscription")
-		subscription.party_type = "Customer"
-		subscription.party = "_Test Customer"
-		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
-		subscription.save()
+		subscription = create_subscription()
 		subscription.cancel_subscription()
 
 		invoice = subscription.get_current_invoice()
@@ -421,18 +254,13 @@
 		settings.prorate = to_prorate
 		settings.save()
 
-	def test_subcription_cancellation_and_process(self):
+	def test_subscription_cancellation_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("Subscription")
-		subscription.party_type = "Customer"
-		subscription.party = "_Test Customer"
-		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
-		subscription.start_date = "2018-01-01"
-		subscription.insert()
+		subscription = create_subscription(start_date="2018-01-01")
 		subscription.process()  # generate first invoice
 
 		# Generate an invoice for the cancelled period
@@ -458,14 +286,8 @@
 		settings.cancel_after_grace = 0
 		settings.save()
 
-		subscription = frappe.new_doc("Subscription")
-		subscription.party_type = "Customer"
-		subscription.party = "_Test Customer"
-		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
+		subscription = create_subscription(start_date="2018-01-01")
+		subscription.process(posting_date="2018-01-31")  # generate first invoice
 
 		# Status is unpaid as Days until Due is zero and grace period is Zero
 		self.assertEqual(subscription.status, "Unpaid")
@@ -494,17 +316,10 @@
 		settings.cancel_after_grace = 0
 		settings.save()
 
-		subscription = frappe.new_doc("Subscription")
-		subscription.party_type = "Customer"
-		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
+		subscription = create_subscription(
+			start_date="2018-01-01", generate_invoice_at="Beginning of the current subscription period"
+		)
+		subscription.process(subscription.current_invoice_start)  # generate first invoice
 		# This should change status to Unpaid since grace period is 0
 		self.assertEqual(subscription.status, "Unpaid")
 
@@ -516,29 +331,18 @@
 		self.assertEqual(subscription.status, "Active")
 
 		# A new invoice is generated
-		frappe.flags.current_date = subscription.current_invoice_start
-		subscription.process()
+		subscription.process(posting_date=subscription.current_invoice_start)
 		self.assertEqual(subscription.status, "Unpaid")
 
 		settings.cancel_after_grace = default_grace_period_action
 		settings.save()
 
 	def test_restart_active_subscription(self):
-		subscription = frappe.new_doc("Subscription")
-		subscription.party_type = "Customer"
-		subscription.party = "_Test Customer"
-		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
-		subscription.save()
-
+		subscription = create_subscription()
 		self.assertRaises(frappe.ValidationError, subscription.restart_subscription)
 
 	def test_subscription_invoice_discount_percentage(self):
-		subscription = frappe.new_doc("Subscription")
-		subscription.party_type = "Customer"
-		subscription.party = "_Test Customer"
-		subscription.additional_discount_percentage = 10
-		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
-		subscription.save()
+		subscription = create_subscription(additional_discount_percentage=10)
 		subscription.cancel_subscription()
 
 		invoice = subscription.get_current_invoice()
@@ -547,12 +351,7 @@
 		self.assertEqual(invoice.apply_discount_on, "Grand Total")
 
 	def test_subscription_invoice_discount_amount(self):
-		subscription = frappe.new_doc("Subscription")
-		subscription.party_type = "Customer"
-		subscription.party = "_Test Customer"
-		subscription.additional_discount_amount = 11
-		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
-		subscription.save()
+		subscription = create_subscription(additional_discount_amount=11)
 		subscription.cancel_subscription()
 
 		invoice = subscription.get_current_invoice()
@@ -563,18 +362,13 @@
 	def test_prepaid_subscriptions(self):
 		# Create a non pre-billed subscription, processing should not create
 		# invoices.
-		subscription = frappe.new_doc("Subscription")
-		subscription.party_type = "Customer"
-		subscription.party = "_Test Customer"
-		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
-		subscription.save()
+		subscription = create_subscription()
 		subscription.process()
-
 		self.assertEqual(len(subscription.invoices), 0)
 
 		# Change the subscription type to prebilled and process it.
 		# Prepaid invoice should be generated
-		subscription.generate_invoice_at_period_start = True
+		subscription.generate_invoice_at = "Beginning of the current subscription period"
 		subscription.save()
 		subscription.process()
 
@@ -586,12 +380,9 @@
 		settings.prorate = 1
 		settings.save()
 
-		subscription = frappe.new_doc("Subscription")
-		subscription.party_type = "Customer"
-		subscription.party = "_Test Customer"
-		subscription.generate_invoice_at_period_start = True
-		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
-		subscription.save()
+		subscription = create_subscription(
+			generate_invoice_at="Beginning of the current subscription period"
+		)
 		subscription.process()
 		subscription.cancel_subscription()
 
@@ -609,9 +400,10 @@
 
 	def test_subscription_with_follow_calendar_months(self):
 		subscription = frappe.new_doc("Subscription")
+		subscription.company = "_Test Company"
 		subscription.party_type = "Supplier"
 		subscription.party = "_Test Supplier"
-		subscription.generate_invoice_at_period_start = 1
+		subscription.generate_invoice_at = "Beginning of the current subscription period"
 		subscription.follow_calendar_months = 1
 
 		# select subscription start date as "2018-01-15"
@@ -625,39 +417,33 @@
 		self.assertEqual(get_date_str(subscription.current_invoice_end), "2018-03-31")
 
 	def test_subscription_generate_invoice_past_due(self):
-		subscription = frappe.new_doc("Subscription")
-		subscription.party_type = "Supplier"
-		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"
-		subscription.start_date = "2018-01-01"
-		subscription.append("plans", {"plan": "_Test Plan Name 4", "qty": 1})
-		subscription.save()
+		subscription = create_subscription(
+			start_date="2018-01-01",
+			party_type="Supplier",
+			party="_Test Supplier",
+			generate_invoice_at="Beginning of the current subscription period",
+			generate_new_invoices_past_due_date=1,
+			plans=[{"plan": "_Test Plan Name 4", "qty": 1}],
+		)
 
-		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()
+		subscription.process(posting_date="2018-01-01")
 		self.assertEqual(len(subscription.invoices), 1)
 		self.assertEqual(subscription.status, "Unpaid")
 
 		# Now the Subscription is unpaid
 		# Even then new invoice should be created as we have enabled `generate_new_invoices_past_due_date` in
 		# subscription and the interval between the subscriptions is 3 months
-		frappe.flags.current_date = "2018-04-01"
-		subscription.process()
+		subscription.process(posting_date="2018-04-01")
 		self.assertEqual(len(subscription.invoices), 2)
 
 	def test_subscription_without_generate_invoice_past_due(self):
-		subscription = frappe.new_doc("Subscription")
-		subscription.party_type = "Supplier"
-		subscription.party = "_Test Supplier"
-		subscription.generate_invoice_at_period_start = 1
-		# 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()
+		subscription = create_subscription(
+			start_date="2018-01-01",
+			generate_invoice_at="Beginning of the current subscription period",
+			plans=[{"plan": "_Test Plan Name 4", "qty": 1}],
+		)
 
 		# Process subscription and create first invoice
 		# Subscription status will be unpaid since due date has already passed
@@ -668,16 +454,13 @@
 		subscription.process()
 		self.assertEqual(len(subscription.invoices), 1)
 
-	def test_multicurrency_subscription(self):
-		subscription = frappe.new_doc("Subscription")
-		subscription.party_type = "Customer"
-		subscription.party = "_Test Subscription Customer"
-		subscription.generate_invoice_at_period_start = 1
-		subscription.company = "_Test Company"
-		# 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()
+	def test_multi_currency_subscription(self):
+		subscription = create_subscription(
+			start_date="2018-01-01",
+			generate_invoice_at="Beginning of the current subscription period",
+			plans=[{"plan": "_Test Plan Multicurrency", "qty": 1}],
+			party="_Test Subscription Customer",
+		)
 
 		subscription.process()
 		self.assertEqual(len(subscription.invoices), 1)
@@ -689,42 +472,135 @@
 
 	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()
+		subscription = create_subscription(
+			start_date="2021-01-01",
+			submit_invoice=0,
+			generate_new_invoices_past_due_date=1,
+			party="_Test Subscription Customer",
+		)
 
 		# create invoices for the first two moths
-		frappe.flags.current_date = "2021-12-31"
-		subscription.process()
+		subscription.process(posting_date="2021-01-31")
 
-		frappe.flags.current_date = "2022-01-31"
-		subscription.process()
+		subscription.process(posting_date="2021-02-28")
 
 		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"),
+			getdate("2021-01-01"),
 		)
 		self.assertEqual(
 			getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[1].name, "from_date")),
-			getdate("2022-01-01"),
+			getdate("2021-02-01"),
 		)
 
 		# recreate most recent invoice
-		subscription.process()
+		subscription.process(posting_date="2022-01-31")
 
 		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"),
+			getdate("2021-01-01"),
 		)
 		self.assertEqual(
 			getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[1].name, "from_date")),
-			getdate("2022-01-01"),
+			getdate("2021-02-01"),
 		)
+
+	def test_subscription_invoice_generation_before_days(self):
+		subscription = create_subscription(
+			start_date="2023-01-01",
+			generate_invoice_at="Days before the current subscription period",
+			number_of_days=10,
+			generate_new_invoices_past_due_date=1,
+		)
+
+		subscription.process(posting_date="2022-12-22")
+		self.assertEqual(len(subscription.invoices), 1)
+
+		subscription.process(posting_date="2023-01-22")
+		self.assertEqual(len(subscription.invoices), 2)
+
+
+def make_plans():
+	create_plan(plan_name="_Test Plan Name", cost=900)
+	create_plan(plan_name="_Test Plan Name 2", cost=1999)
+	create_plan(
+		plan_name="_Test Plan Name 3", cost=1999, billing_interval="Day", billing_interval_count=14
+	)
+	create_plan(
+		plan_name="_Test Plan Name 4", cost=20000, billing_interval="Month", billing_interval_count=3
+	)
+	create_plan(
+		plan_name="_Test Plan Multicurrency", cost=50, billing_interval="Month", currency="USD"
+	)
+
+
+def create_plan(**kwargs):
+	if not frappe.db.exists("Subscription Plan", kwargs.get("plan_name")):
+		plan = frappe.new_doc("Subscription Plan")
+		plan.plan_name = kwargs.get("plan_name") or "_Test Plan Name"
+		plan.item = kwargs.get("item") or "_Test Non Stock Item"
+		plan.price_determination = kwargs.get("price_determination") or "Fixed Rate"
+		plan.cost = kwargs.get("cost") or 1000
+		plan.billing_interval = kwargs.get("billing_interval") or "Month"
+		plan.billing_interval_count = kwargs.get("billing_interval_count") or 1
+		plan.currency = kwargs.get("currency")
+		plan.insert()
+
+
+def create_parties():
+	if not frappe.db.exists("Supplier", "_Test Supplier"):
+		supplier = frappe.new_doc("Supplier")
+		supplier.supplier_name = "_Test Supplier"
+		supplier.supplier_group = "All Supplier Groups"
+		supplier.insert()
+
+	if not frappe.db.exists("Customer", "_Test Subscription Customer"):
+		customer = frappe.new_doc("Customer")
+		customer.customer_name = "_Test Subscription Customer"
+		customer.billing_currency = "USD"
+		customer.append(
+			"accounts", {"company": "_Test Company", "account": "_Test Receivable USD - _TC"}
+		)
+		customer.insert()
+
+
+def reset_settings():
+	settings = frappe.get_single("Subscription Settings")
+	settings.grace_period = 0
+	settings.cancel_after_grace = 0
+	settings.save()
+
+
+def create_subscription(**kwargs):
+	subscription = frappe.new_doc("Subscription")
+	subscription.party_type = (kwargs.get("party_type") or "Customer",)
+	subscription.company = kwargs.get("company") or "_Test Company"
+	subscription.party = kwargs.get("party") or "_Test Customer"
+	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.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")
+	subscription.follow_calendar_months = kwargs.get("follow_calendar_months")
+	subscription.generate_new_invoices_past_due_date = kwargs.get(
+		"generate_new_invoices_past_due_date"
+	)
+	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")
+
+	if not kwargs.get("plans"):
+		subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
+	else:
+		for plan in kwargs.get("plans"):
+			subscription.append("plans", plan)
+
+	if kwargs.get("do_not_save"):
+		return subscription
+
+	subscription.save()
+
+	return subscription
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 7bf8fb4..2155699 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -430,7 +430,7 @@
 		"erpnext.projects.doctype.project.project.collect_project_status",
 	],
 	"hourly_long": [
-		"erpnext.accounts.doctype.subscription.subscription.process_all",
+		"erpnext.accounts.doctype.process_subscription.process_subscription.create_subscription_process",
 		"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries",
 		"erpnext.bulk_transaction.doctype.bulk_transaction_log.bulk_transaction_log.retry_failing_transaction",
 	],
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 3433e3c..e9c056e 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -342,6 +342,7 @@
 erpnext.patches.v15_0.correct_asset_value_if_je_with_workflow
 erpnext.patches.v15_0.delete_woocommerce_settings_doctype
 erpnext.patches.v14_0.migrate_deferred_accounts_to_item_defaults
+erpnext.patches.v14_0.update_invoicing_period_in_subscription
 execute:frappe.delete_doc("Page", "welcome-to-erpnext")
 # below migration patch should always run last
 erpnext.patches.v14_0.migrate_gl_to_payment_ledger
diff --git a/erpnext/patches/v14_0/update_invoicing_period_in_subscription.py b/erpnext/patches/v14_0/update_invoicing_period_in_subscription.py
new file mode 100644
index 0000000..2879e57
--- /dev/null
+++ b/erpnext/patches/v14_0/update_invoicing_period_in_subscription.py
@@ -0,0 +1,8 @@
+import frappe
+
+
+def execute():
+	subscription = frappe.qb.DocType("Subscription")
+	frappe.qb.update(subscription).set(
+		subscription.generate_invoice_at, "Beginning of the currency subscription period"
+	).where(subscription.generate_invoice_at_period_start == 1).run()