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
This commit is contained in:
Deepesh Garg 2023-09-19 18:39:44 +05:30 committed by GitHub
parent 03f0abf6de
commit e19e04b050
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 406 additions and 364 deletions

View File

@ -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) {
// },
// });

View File

@ -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": []
}

View File

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

View File

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

View File

@ -24,8 +24,9 @@
"current_invoice_start", "current_invoice_start",
"current_invoice_end", "current_invoice_end",
"days_until_due", "days_until_due",
"generate_invoice_at",
"number_of_days",
"cancel_at_period_end", "cancel_at_period_end",
"generate_invoice_at_period_start",
"sb_4", "sb_4",
"plans", "plans",
"sb_1", "sb_1",
@ -86,12 +87,14 @@
"fieldname": "current_invoice_start", "fieldname": "current_invoice_start",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Current Invoice Start Date", "label": "Current Invoice Start Date",
"no_copy": 1,
"read_only": 1 "read_only": 1
}, },
{ {
"fieldname": "current_invoice_end", "fieldname": "current_invoice_end",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Current Invoice End Date", "label": "Current Invoice End Date",
"no_copy": 1,
"read_only": 1 "read_only": 1
}, },
{ {
@ -107,12 +110,6 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Cancel At End Of Period" "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, "allow_on_submit": 1,
"fieldname": "sb_4", "fieldname": "sb_4",
@ -240,6 +237,21 @@
"fieldname": "submit_invoice", "fieldname": "submit_invoice",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Submit Generated Invoices" "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, "index_web_pages_for_search": 1,
@ -255,7 +267,7 @@
"link_fieldname": "subscription" "link_fieldname": "subscription"
} }
], ],
"modified": "2022-02-18 23:24:57.185054", "modified": "2023-09-18 17:48:21.900252",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Subscription", "name": "Subscription",

View File

@ -36,12 +36,15 @@ class InvoiceNotCancelled(frappe.ValidationError):
pass pass
DateTimeLikeObject = Union[str, datetime.date]
class Subscription(Document): class Subscription(Document):
def before_insert(self): def before_insert(self):
# update start just before the subscription doc is created # update start just before the subscription doc is created
self.update_subscription_period(self.start_date) 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 Subscription period is the period to be billed. This method updates the
beginning of the billing period and end of the billing period. beginning of the billing period and end of the billing period.
@ -52,14 +55,14 @@ class Subscription(Document):
self.current_invoice_start = self.get_current_invoice_start(date) self.current_invoice_start = self.get_current_invoice_start(date)
self.current_invoice_end = self.get_current_invoice_end(self.current_invoice_start) 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_start = self.get_current_invoice_start(date)
_current_invoice_end = self.get_current_invoice_end(_current_invoice_start) _current_invoice_end = self.get_current_invoice_end(_current_invoice_start)
return _current_invoice_start, _current_invoice_end return _current_invoice_start, _current_invoice_end
def get_current_invoice_start( def get_current_invoice_start(
self, date: Optional[Union[datetime.date, str]] = None self, date: Optional["DateTimeLikeObject"] = None
) -> Union[datetime.date, str]: ) -> Union[datetime.date, str]:
""" """
This returns the date of the beginning of the current billing period. This returns the date of the beginning of the current billing period.
@ -84,7 +87,7 @@ class Subscription(Document):
return _current_invoice_start return _current_invoice_start
def get_current_invoice_end( def get_current_invoice_end(
self, date: Optional[Union[datetime.date, str]] = None self, date: Optional["DateTimeLikeObject"] = None
) -> Union[datetime.date, str]: ) -> Union[datetime.date, str]:
""" """
This returns the date of the end of the current billing period. This returns the date of the end of the current billing period.
@ -179,30 +182,24 @@ class Subscription(Document):
return data 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` Sets the status of the `Subscription`
""" """
if self.is_trialling(): if self.is_trialling():
self.status = "Trialling" self.status = "Trialling"
elif ( elif (
self.status == "Active" self.status == "Active" and self.end_date and getdate(posting_date) > getdate(self.end_date)
and self.end_date
and getdate(frappe.flags.current_date) > getdate(self.end_date)
): ):
self.status = "Completed" self.status = "Completed"
elif self.is_past_grace_period(): elif self.is_past_grace_period():
self.status = self.get_status_for_past_grace_period() self.status = self.get_status_for_past_grace_period()
self.cancelation_date = ( self.cancelation_date = getdate(posting_date) if self.status == "Cancelled" else None
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(): elif self.current_invoice_is_past_due() and not self.is_past_grace_period():
self.status = "Past Due Date" self.status = "Past Due Date"
elif not self.has_outstanding_invoice() or self.is_new_subscription(): elif not self.has_outstanding_invoice() or self.is_new_subscription():
self.status = "Active" self.status = "Active"
self.save()
def is_trialling(self) -> bool: def is_trialling(self) -> bool:
""" """
Returns `True` if the `Subscription` is in trial period. Returns `True` if the `Subscription` is in trial period.
@ -210,7 +207,9 @@ class Subscription(Document):
return not self.period_has_passed(self.trial_period_end) and self.is_new_subscription() return not self.period_has_passed(self.trial_period_end) and self.is_new_subscription()
@staticmethod @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 Returns true if the given `end_date` has passed
""" """
@ -218,7 +217,7 @@ class Subscription(Document):
if not end_date: if not end_date:
return True 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: def get_status_for_past_grace_period(self) -> str:
cancel_after_grace = cint(frappe.get_value("Subscription Settings", None, "cancel_after_grace")) cancel_after_grace = cint(frappe.get_value("Subscription Settings", None, "cancel_after_grace"))
@ -229,7 +228,7 @@ class Subscription(Document):
return status 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 Returns `True` if the grace period for the `Subscription` has passed
""" """
@ -237,18 +236,18 @@ class Subscription(Document):
return return
grace_period = cint(frappe.get_value("Subscription Settings", None, "grace_period")) grace_period = cint(frappe.get_value("Subscription Settings", None, "grace_period"))
return getdate(frappe.flags.current_date) >= getdate( return getdate(posting_date) >= getdate(add_days(self.current_invoice.due_date, grace_period))
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 Returns `True` if the current generated invoice is overdue
""" """
if not self.current_invoice or self.is_paid(self.current_invoice): if not self.current_invoice or self.is_paid(self.current_invoice):
return False 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 @property
def invoice_document_type(self) -> str: def invoice_document_type(self) -> str:
@ -270,6 +269,9 @@ class Subscription(Document):
if not self.cost_center: if not self.cost_center:
self.cost_center = get_default_cost_center(self.get("company")) self.cost_center = get_default_cost_center(self.get("company"))
if self.is_new():
self.set_subscription_status()
def validate_trial_period(self) -> None: def validate_trial_period(self) -> None:
""" """
Runs sanity checks on trial period dates for the `Subscription` Runs sanity checks on trial period dates for the `Subscription`
@ -305,10 +307,6 @@ class Subscription(Document):
if billing_info[0]["billing_interval"] != "Month": if billing_info[0]["billing_interval"] != "Month":
frappe.throw(_("Billing Interval in Subscription Plan must be Month to follow calendar months")) 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( def generate_invoice(
self, self,
from_date: Optional[Union[str, datetime.date]] = None, from_date: Optional[Union[str, datetime.date]] = None,
@ -344,7 +342,7 @@ class Subscription(Document):
invoice.set_posting_time = 1 invoice.set_posting_time = 1
invoice.posting_date = ( invoice.posting_date = (
self.current_invoice_start 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 else self.current_invoice_end
) )
@ -438,7 +436,7 @@ class Subscription(Document):
prorate_factor = get_prorata_factor( prorate_factor = get_prorata_factor(
self.current_invoice_end, self.current_invoice_end,
self.current_invoice_start, self.current_invoice_start,
cint(self.generate_invoice_at_period_start), cint(self.generate_invoice_at == "Beginning of the current subscription period"),
) )
items = [] items = []
@ -503,42 +501,45 @@ class Subscription(Document):
return items return items
@frappe.whitelist() @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 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: as need be. It calls either of these methods depending the `Subscription` status:
1. `process_for_active` 1. `process_for_active`
2. `process_for_past_due` 2. `process_for_past_due`
""" """
if ( if not self.is_current_invoice_generated(
not self.is_current_invoice_generated(self.current_invoice_start, self.current_invoice_end) self.current_invoice_start, self.current_invoice_end
and self.can_generate_new_invoice() ) and self.can_generate_new_invoice(posting_date):
):
self.generate_invoice() self.generate_invoice()
self.update_subscription_period(add_days(self.current_invoice_end, 1)) self.update_subscription_period(add_days(self.current_invoice_end, 1))
if self.cancel_at_period_end and ( if self.cancel_at_period_end and (
getdate(frappe.flags.current_date) >= getdate(self.current_invoice_end) getdate(posting_date) >= getdate(self.current_invoice_end)
or getdate(frappe.flags.current_date) >= getdate(self.end_date) or getdate(posting_date) >= getdate(self.end_date)
): ):
self.cancel_subscription() self.cancel_subscription()
self.set_subscription_status() self.set_subscription_status(posting_date=posting_date)
self.save() 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: if self.cancelation_date:
return False return False
elif self.generate_invoice_at_period_start and (
getdate(frappe.flags.current_date) == getdate(self.current_invoice_start) if self.has_outstanding_invoice() and not self.generate_new_invoices_past_due_date:
or self.is_new_subscription() 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 return True
elif getdate(frappe.flags.current_date) == getdate(self.current_invoice_end): elif self.generate_invoice_at == "Days before the current subscription period" and (
if self.has_outstanding_invoice() and not self.generate_new_invoices_past_due_date: getdate(posting_date) == getdate(add_days(self.current_invoice_start, -1 * self.number_of_days))
return False ):
return True
elif getdate(posting_date) == getdate(self.current_invoice_end):
return True return True
else: else:
return False return False
@ -628,7 +629,10 @@ class Subscription(Document):
frappe.throw(_("subscription is already cancelled."), InvoiceCancelled) frappe.throw(_("subscription is already cancelled."), InvoiceCancelled)
to_generate_invoice = ( 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.status = "Cancelled"
self.cancelation_date = nowdate() self.cancelation_date = nowdate()
@ -639,7 +643,7 @@ class Subscription(Document):
self.save() self.save()
@frappe.whitelist() @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 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 subscription and the `Subscription` will lose all the history of generated invoices
@ -650,7 +654,7 @@ class Subscription(Document):
self.status = "Active" self.status = "Active"
self.cancelation_date = None self.cancelation_date = None
self.update_subscription_period(frappe.flags.current_date or nowdate()) self.update_subscription_period(posting_date or nowdate())
self.save() self.save()
@ -671,14 +675,21 @@ def get_prorata_factor(
return diff / plan_days 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 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: try:
subscription = frappe.get_doc("Subscription", subscription) subscription = frappe.get_doc("Subscription", subscription)
subscription.process() subscription.process(posting_date)
frappe.db.commit() frappe.db.commit()
except frappe.ValidationError: except frappe.ValidationError:
frappe.db.rollback() frappe.db.rollback()

View File

@ -8,6 +8,7 @@ from frappe.utils.data import (
add_days, add_days,
add_months, add_months,
add_to_date, add_to_date,
cint,
date_diff, date_diff,
flt, flt,
get_date_str, get_date_str,
@ -20,99 +21,16 @@ from erpnext.accounts.doctype.subscription.subscription import get_prorata_facto
test_dependencies = ("UOM", "Item Group", "Item") 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): class TestSubscription(unittest.TestCase):
def setUp(self): def setUp(self):
create_plan() make_plans()
create_parties() create_parties()
reset_settings() reset_settings()
def test_create_subscription_with_trial_with_correct_period(self): def test_create_subscription_with_trial_with_correct_period(self):
subscription = frappe.new_doc("Subscription") subscription = create_subscription(
subscription.party_type = "Customer" trial_period_start=nowdate(), trial_period_end=add_months(nowdate(), 1)
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()
self.assertEqual(subscription.trial_period_start, nowdate()) self.assertEqual(subscription.trial_period_start, nowdate())
self.assertEqual(subscription.trial_period_end, add_months(nowdate(), 1)) self.assertEqual(subscription.trial_period_end, add_months(nowdate(), 1))
self.assertEqual( self.assertEqual(
@ -126,12 +44,7 @@ class TestSubscription(unittest.TestCase):
self.assertEqual(subscription.status, "Trialling") self.assertEqual(subscription.status, "Trialling")
def test_create_subscription_without_trial_with_correct_period(self): def test_create_subscription_without_trial_with_correct_period(self):
subscription = frappe.new_doc("Subscription") subscription = create_subscription()
subscription.party_type = "Customer"
subscription.party = "_Test Customer"
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
subscription.save()
self.assertEqual(subscription.trial_period_start, None) self.assertEqual(subscription.trial_period_start, None)
self.assertEqual(subscription.trial_period_end, None) self.assertEqual(subscription.trial_period_end, None)
self.assertEqual(subscription.current_invoice_start, nowdate()) self.assertEqual(subscription.current_invoice_start, nowdate())
@ -141,55 +54,28 @@ class TestSubscription(unittest.TestCase):
self.assertEqual(subscription.status, "Active") self.assertEqual(subscription.status, "Active")
def test_create_subscription_trial_with_wrong_dates(self): def test_create_subscription_trial_with_wrong_dates(self):
subscription = frappe.new_doc("Subscription") subscription = create_subscription(
subscription.party_type = "Customer" trial_period_start=add_days(nowdate(), 30), trial_period_end=nowdate(), do_not_save=True
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})
self.assertRaises(frappe.ValidationError, subscription.save) self.assertRaises(frappe.ValidationError, subscription.save)
def test_invoice_is_generated_at_end_of_billing_period(self): def test_invoice_is_generated_at_end_of_billing_period(self):
subscription = frappe.new_doc("Subscription") subscription = create_subscription(start_date="2018-01-01")
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()
self.assertEqual(subscription.status, "Active") self.assertEqual(subscription.status, "Active")
self.assertEqual(subscription.current_invoice_start, "2018-01-01") self.assertEqual(subscription.current_invoice_start, "2018-01-01")
self.assertEqual(subscription.current_invoice_end, "2018-01-31") 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(len(subscription.invoices), 1)
self.assertEqual(subscription.current_invoice_start, "2018-02-01") self.assertEqual(subscription.current_invoice_start, "2018-02-01")
self.assertEqual(subscription.current_invoice_end, "2018-02-28") self.assertEqual(subscription.current_invoice_end, "2018-02-28")
self.assertEqual(subscription.status, "Unpaid") self.assertEqual(subscription.status, "Unpaid")
def test_status_goes_back_to_active_after_invoice_is_paid(self): def test_status_goes_back_to_active_after_invoice_is_paid(self):
subscription = frappe.new_doc("Subscription") subscription = create_subscription(
subscription.party_type = "Customer" start_date="2018-01-01", generate_invoice_at="Beginning of the current subscription period"
subscription.party = "_Test Customer" )
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) subscription.process(posting_date="2018-01-01") # generate first invoice
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) self.assertEqual(len(subscription.invoices), 1)
# Status is unpaid as Days until Due is zero and grace period is Zero # Status is unpaid as Days until Due is zero and grace period is Zero
@ -213,18 +99,10 @@ class TestSubscription(unittest.TestCase):
settings.cancel_after_grace = 1 settings.cancel_after_grace = 1
settings.save() settings.save()
subscription = frappe.new_doc("Subscription") subscription = create_subscription(start_date="2018-01-01")
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") self.assertEqual(subscription.status, "Active")
frappe.flags.current_date = "2018-01-31" subscription.process(posting_date="2018-01-31") # generate first invoice
subscription.process() # generate first invoice
# This should change status to Cancelled since grace period is 0 # This should change status to Cancelled since grace period is 0
# And is backdated subscription so subscription will be cancelled after processing # And is backdated subscription so subscription will be cancelled after processing
self.assertEqual(subscription.status, "Cancelled") self.assertEqual(subscription.status, "Cancelled")
@ -235,13 +113,8 @@ class TestSubscription(unittest.TestCase):
settings.cancel_after_grace = 0 settings.cancel_after_grace = 0
settings.save() settings.save()
subscription = frappe.new_doc("Subscription") subscription = create_subscription(start_date="2018-01-01")
subscription.party_type = "Customer" subscription.process(posting_date="2018-01-31") # generate first invoice
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
# Status is unpaid as Days until Due is zero and grace period is Zero # Status is unpaid as Days until Due is zero and grace period is Zero
self.assertEqual(subscription.status, "Unpaid") self.assertEqual(subscription.status, "Unpaid")
@ -251,21 +124,9 @@ class TestSubscription(unittest.TestCase):
def test_subscription_invoice_days_until_due(self): def test_subscription_invoice_days_until_due(self):
_date = add_months(nowdate(), -1) _date = add_months(nowdate(), -1)
subscription = frappe.new_doc("Subscription") subscription = create_subscription(start_date=_date, days_until_due=10)
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()
frappe.flags.current_date = subscription.current_invoice_end subscription.process(posting_date=subscription.current_invoice_end) # generate first invoice
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)
self.assertEqual(len(subscription.invoices), 1) self.assertEqual(len(subscription.invoices), 1)
self.assertEqual(subscription.status, "Active") self.assertEqual(subscription.status, "Active")
@ -275,16 +136,9 @@ class TestSubscription(unittest.TestCase):
settings.grace_period = 1000 settings.grace_period = 1000
settings.save() settings.save()
subscription = frappe.new_doc("Subscription") subscription = create_subscription(start_date=add_days(nowdate(), -1000))
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()
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") self.assertEqual(subscription.status, "Past Due Date")
subscription.process() subscription.process()
@ -301,12 +155,7 @@ class TestSubscription(unittest.TestCase):
settings.save() settings.save()
def test_subscription_remains_active_during_invoice_period(self): def test_subscription_remains_active_during_invoice_period(self):
subscription = frappe.new_doc("Subscription") subscription = create_subscription() # no changes expected
subscription.party_type = "Customer"
subscription.party = "_Test Customer"
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
subscription.save()
subscription.process() # no changes expected
self.assertEqual(subscription.status, "Active") self.assertEqual(subscription.status, "Active")
self.assertEqual(subscription.current_invoice_start, nowdate()) self.assertEqual(subscription.current_invoice_start, nowdate())
@ -325,12 +174,8 @@ class TestSubscription(unittest.TestCase):
self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1)) self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1))
self.assertEqual(len(subscription.invoices), 0) self.assertEqual(len(subscription.invoices), 0)
def test_subscription_cancelation(self): def test_subscription_cancellation(self):
subscription = frappe.new_doc("Subscription") subscription = create_subscription()
subscription.party_type = "Customer"
subscription.party = "_Test Customer"
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
subscription.save()
subscription.cancel_subscription() subscription.cancel_subscription()
self.assertEqual(subscription.status, "Cancelled") self.assertEqual(subscription.status, "Cancelled")
@ -341,11 +186,7 @@ class TestSubscription(unittest.TestCase):
settings.prorate = 1 settings.prorate = 1
settings.save() settings.save()
subscription = frappe.new_doc("Subscription") subscription = create_subscription()
subscription.party_type = "Customer"
subscription.party = "_Test Customer"
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
subscription.save()
self.assertEqual(subscription.status, "Active") self.assertEqual(subscription.status, "Active")
@ -365,7 +206,7 @@ class TestSubscription(unittest.TestCase):
get_prorata_factor( get_prorata_factor(
subscription.current_invoice_end, subscription.current_invoice_end,
subscription.current_invoice_start, subscription.current_invoice_start,
subscription.generate_invoice_at_period_start, cint(subscription.generate_invoice_at == "Beginning of the current subscription period"),
), ),
2, 2,
), ),
@ -383,11 +224,7 @@ class TestSubscription(unittest.TestCase):
settings.prorate = 0 settings.prorate = 0
settings.save() settings.save()
subscription = frappe.new_doc("Subscription") subscription = create_subscription()
subscription.party_type = "Customer"
subscription.party = "_Test Customer"
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
subscription.save()
subscription.cancel_subscription() subscription.cancel_subscription()
invoice = subscription.get_current_invoice() invoice = subscription.get_current_invoice()
@ -402,11 +239,7 @@ class TestSubscription(unittest.TestCase):
settings.prorate = 1 settings.prorate = 1
settings.save() settings.save()
subscription = frappe.new_doc("Subscription") subscription = create_subscription()
subscription.party_type = "Customer"
subscription.party = "_Test Customer"
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
subscription.save()
subscription.cancel_subscription() subscription.cancel_subscription()
invoice = subscription.get_current_invoice() invoice = subscription.get_current_invoice()
@ -421,18 +254,13 @@ class TestSubscription(unittest.TestCase):
settings.prorate = to_prorate settings.prorate = to_prorate
settings.save() settings.save()
def test_subcription_cancellation_and_process(self): def test_subscription_cancellation_and_process(self):
settings = frappe.get_single("Subscription Settings") settings = frappe.get_single("Subscription Settings")
default_grace_period_action = settings.cancel_after_grace default_grace_period_action = settings.cancel_after_grace
settings.cancel_after_grace = 1 settings.cancel_after_grace = 1
settings.save() settings.save()
subscription = frappe.new_doc("Subscription") subscription = create_subscription(start_date="2018-01-01")
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.process() # generate first invoice
# Generate an invoice for the cancelled period # Generate an invoice for the cancelled period
@ -458,14 +286,8 @@ class TestSubscription(unittest.TestCase):
settings.cancel_after_grace = 0 settings.cancel_after_grace = 0
settings.save() settings.save()
subscription = frappe.new_doc("Subscription") subscription = create_subscription(start_date="2018-01-01")
subscription.party_type = "Customer" subscription.process(posting_date="2018-01-31") # generate first invoice
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
# Status is unpaid as Days until Due is zero and grace period is Zero # Status is unpaid as Days until Due is zero and grace period is Zero
self.assertEqual(subscription.status, "Unpaid") self.assertEqual(subscription.status, "Unpaid")
@ -494,17 +316,10 @@ class TestSubscription(unittest.TestCase):
settings.cancel_after_grace = 0 settings.cancel_after_grace = 0
settings.save() settings.save()
subscription = frappe.new_doc("Subscription") subscription = create_subscription(
subscription.party_type = "Customer" start_date="2018-01-01", generate_invoice_at="Beginning of the current subscription period"
subscription.party = "_Test Customer" )
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) subscription.process(subscription.current_invoice_start) # generate first invoice
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 # This should change status to Unpaid since grace period is 0
self.assertEqual(subscription.status, "Unpaid") self.assertEqual(subscription.status, "Unpaid")
@ -516,29 +331,18 @@ class TestSubscription(unittest.TestCase):
self.assertEqual(subscription.status, "Active") self.assertEqual(subscription.status, "Active")
# A new invoice is generated # A new invoice is generated
frappe.flags.current_date = subscription.current_invoice_start subscription.process(posting_date=subscription.current_invoice_start)
subscription.process()
self.assertEqual(subscription.status, "Unpaid") self.assertEqual(subscription.status, "Unpaid")
settings.cancel_after_grace = default_grace_period_action settings.cancel_after_grace = default_grace_period_action
settings.save() settings.save()
def test_restart_active_subscription(self): def test_restart_active_subscription(self):
subscription = frappe.new_doc("Subscription") subscription = create_subscription()
subscription.party_type = "Customer"
subscription.party = "_Test Customer"
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
subscription.save()
self.assertRaises(frappe.ValidationError, subscription.restart_subscription) self.assertRaises(frappe.ValidationError, subscription.restart_subscription)
def test_subscription_invoice_discount_percentage(self): def test_subscription_invoice_discount_percentage(self):
subscription = frappe.new_doc("Subscription") subscription = create_subscription(additional_discount_percentage=10)
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.cancel_subscription() subscription.cancel_subscription()
invoice = subscription.get_current_invoice() invoice = subscription.get_current_invoice()
@ -547,12 +351,7 @@ class TestSubscription(unittest.TestCase):
self.assertEqual(invoice.apply_discount_on, "Grand Total") self.assertEqual(invoice.apply_discount_on, "Grand Total")
def test_subscription_invoice_discount_amount(self): def test_subscription_invoice_discount_amount(self):
subscription = frappe.new_doc("Subscription") subscription = create_subscription(additional_discount_amount=11)
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.cancel_subscription() subscription.cancel_subscription()
invoice = subscription.get_current_invoice() invoice = subscription.get_current_invoice()
@ -563,18 +362,13 @@ class TestSubscription(unittest.TestCase):
def test_prepaid_subscriptions(self): def test_prepaid_subscriptions(self):
# Create a non pre-billed subscription, processing should not create # Create a non pre-billed subscription, processing should not create
# invoices. # invoices.
subscription = frappe.new_doc("Subscription") subscription = create_subscription()
subscription.party_type = "Customer"
subscription.party = "_Test Customer"
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
subscription.save()
subscription.process() subscription.process()
self.assertEqual(len(subscription.invoices), 0) self.assertEqual(len(subscription.invoices), 0)
# Change the subscription type to prebilled and process it. # Change the subscription type to prebilled and process it.
# Prepaid invoice should be generated # 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.save()
subscription.process() subscription.process()
@ -586,12 +380,9 @@ class TestSubscription(unittest.TestCase):
settings.prorate = 1 settings.prorate = 1
settings.save() settings.save()
subscription = frappe.new_doc("Subscription") subscription = create_subscription(
subscription.party_type = "Customer" generate_invoice_at="Beginning of the current subscription period"
subscription.party = "_Test Customer" )
subscription.generate_invoice_at_period_start = True
subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1})
subscription.save()
subscription.process() subscription.process()
subscription.cancel_subscription() subscription.cancel_subscription()
@ -609,9 +400,10 @@ class TestSubscription(unittest.TestCase):
def test_subscription_with_follow_calendar_months(self): def test_subscription_with_follow_calendar_months(self):
subscription = frappe.new_doc("Subscription") subscription = frappe.new_doc("Subscription")
subscription.company = "_Test Company"
subscription.party_type = "Supplier" subscription.party_type = "Supplier"
subscription.party = "_Test 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 subscription.follow_calendar_months = 1
# select subscription start date as "2018-01-15" # select subscription start date as "2018-01-15"
@ -625,39 +417,33 @@ class TestSubscription(unittest.TestCase):
self.assertEqual(get_date_str(subscription.current_invoice_end), "2018-03-31") self.assertEqual(get_date_str(subscription.current_invoice_end), "2018-03-31")
def test_subscription_generate_invoice_past_due(self): def test_subscription_generate_invoice_past_due(self):
subscription = frappe.new_doc("Subscription") subscription = create_subscription(
subscription.party_type = "Supplier" start_date="2018-01-01",
subscription.party = "_Test Supplier" party_type="Supplier",
subscription.generate_invoice_at_period_start = 1 party="_Test Supplier",
subscription.generate_new_invoices_past_due_date = 1 generate_invoice_at="Beginning of the current subscription period",
# select subscription start date as "2018-01-15" generate_new_invoices_past_due_date=1,
subscription.start_date = "2018-01-01" plans=[{"plan": "_Test Plan Name 4", "qty": 1}],
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 # Process subscription and create first invoice
# Subscription status will be unpaid since due date has already passed # 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(len(subscription.invoices), 1)
self.assertEqual(subscription.status, "Unpaid") self.assertEqual(subscription.status, "Unpaid")
# Now the Subscription is unpaid # Now the Subscription is unpaid
# Even then new invoice should be created as we have enabled `generate_new_invoices_past_due_date` in # 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 # subscription and the interval between the subscriptions is 3 months
frappe.flags.current_date = "2018-04-01" subscription.process(posting_date="2018-04-01")
subscription.process()
self.assertEqual(len(subscription.invoices), 2) self.assertEqual(len(subscription.invoices), 2)
def test_subscription_without_generate_invoice_past_due(self): def test_subscription_without_generate_invoice_past_due(self):
subscription = frappe.new_doc("Subscription") subscription = create_subscription(
subscription.party_type = "Supplier" start_date="2018-01-01",
subscription.party = "_Test Supplier" generate_invoice_at="Beginning of the current subscription period",
subscription.generate_invoice_at_period_start = 1 plans=[{"plan": "_Test Plan Name 4", "qty": 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()
# Process subscription and create first invoice # Process subscription and create first invoice
# Subscription status will be unpaid since due date has already passed # Subscription status will be unpaid since due date has already passed
@ -668,16 +454,13 @@ class TestSubscription(unittest.TestCase):
subscription.process() subscription.process()
self.assertEqual(len(subscription.invoices), 1) self.assertEqual(len(subscription.invoices), 1)
def test_multicurrency_subscription(self): def test_multi_currency_subscription(self):
subscription = frappe.new_doc("Subscription") subscription = create_subscription(
subscription.party_type = "Customer" start_date="2018-01-01",
subscription.party = "_Test Subscription Customer" generate_invoice_at="Beginning of the current subscription period",
subscription.generate_invoice_at_period_start = 1 plans=[{"plan": "_Test Plan Multicurrency", "qty": 1}],
subscription.company = "_Test Company" party="_Test Subscription Customer",
# 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()
subscription.process() subscription.process()
self.assertEqual(len(subscription.invoices), 1) self.assertEqual(len(subscription.invoices), 1)
@ -689,42 +472,135 @@ class TestSubscription(unittest.TestCase):
def test_subscription_recovery(self): def test_subscription_recovery(self):
"""Test if Subscription recovers when start/end date run out of sync with created invoices.""" """Test if Subscription recovers when start/end date run out of sync with created invoices."""
subscription = frappe.new_doc("Subscription") subscription = create_subscription(
subscription.party_type = "Customer" start_date="2021-01-01",
subscription.party = "_Test Subscription Customer" submit_invoice=0,
subscription.company = "_Test Company" generate_new_invoices_past_due_date=1,
subscription.start_date = "2021-12-01" party="_Test Subscription Customer",
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 # create invoices for the first two moths
frappe.flags.current_date = "2021-12-31" subscription.process(posting_date="2021-01-31")
subscription.process()
frappe.flags.current_date = "2022-01-31" subscription.process(posting_date="2021-02-28")
subscription.process()
self.assertEqual(len(subscription.invoices), 2) self.assertEqual(len(subscription.invoices), 2)
self.assertEqual( self.assertEqual(
getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "from_date")), getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "from_date")),
getdate("2021-12-01"), getdate("2021-01-01"),
) )
self.assertEqual( self.assertEqual(
getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[1].name, "from_date")), 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 # recreate most recent invoice
subscription.process() subscription.process(posting_date="2022-01-31")
self.assertEqual(len(subscription.invoices), 2) self.assertEqual(len(subscription.invoices), 2)
self.assertEqual( self.assertEqual(
getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "from_date")), getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "from_date")),
getdate("2021-12-01"), getdate("2021-01-01"),
) )
self.assertEqual( self.assertEqual(
getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[1].name, "from_date")), 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

View File

@ -430,7 +430,7 @@ scheduler_events = {
"erpnext.projects.doctype.project.project.collect_project_status", "erpnext.projects.doctype.project.project.collect_project_status",
], ],
"hourly_long": [ "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.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries",
"erpnext.bulk_transaction.doctype.bulk_transaction_log.bulk_transaction_log.retry_failing_transaction", "erpnext.bulk_transaction.doctype.bulk_transaction_log.bulk_transaction_log.retry_failing_transaction",
], ],

View File

@ -342,6 +342,7 @@ execute:frappe.db.set_single_value('Selling Settings', 'allow_negative_rates_for
erpnext.patches.v15_0.correct_asset_value_if_je_with_workflow erpnext.patches.v15_0.correct_asset_value_if_je_with_workflow
erpnext.patches.v15_0.delete_woocommerce_settings_doctype erpnext.patches.v15_0.delete_woocommerce_settings_doctype
erpnext.patches.v14_0.migrate_deferred_accounts_to_item_defaults 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") execute:frappe.delete_doc("Page", "welcome-to-erpnext")
# below migration patch should always run last # below migration patch should always run last
erpnext.patches.v14_0.migrate_gl_to_payment_ledger erpnext.patches.v14_0.migrate_gl_to_payment_ledger

View File

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