Merge branch 'develop' into new-subscription
diff --git a/erpnext/accounts/doctype/subscriber/__init__.py b/erpnext/accounts/doctype/subscriber/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/subscriber/__init__.py
diff --git a/erpnext/accounts/doctype/subscriber/subscriber.js b/erpnext/accounts/doctype/subscriber/subscriber.js
new file mode 100644
index 0000000..ba5cdf9
--- /dev/null
+++ b/erpnext/accounts/doctype/subscriber/subscriber.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Subscriber', {
+ refresh: function(frm) {
+
+ }
+});
diff --git a/erpnext/accounts/doctype/subscriber/subscriber.json b/erpnext/accounts/doctype/subscriber/subscriber.json
new file mode 100644
index 0000000..28a57d8
--- /dev/null
+++ b/erpnext/accounts/doctype/subscriber/subscriber.json
@@ -0,0 +1,126 @@
+{
+ "allow_copy": 0,
+ "allow_guest_to_view": 0,
+ "allow_import": 0,
+ "allow_rename": 0,
+ "autoname": "field:subscriber_name",
+ "beta": 0,
+ "creation": "2018-02-24 11:17:46.809140",
+ "custom": 0,
+ "docstatus": 0,
+ "doctype": "DocType",
+ "document_type": "",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "fields": [
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "subscriber_name",
+ "fieldtype": "Data",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 1,
+ "in_standard_filter": 0,
+ "label": "Subscriber Name",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 1,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "customer",
+ "fieldtype": "Link",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 1,
+ "in_standard_filter": 0,
+ "label": "Customer",
+ "length": 0,
+ "no_copy": 0,
+ "options": "Customer",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 1,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ }
+ ],
+ "has_web_view": 0,
+ "hide_heading": 0,
+ "hide_toolbar": 0,
+ "idx": 0,
+ "image_view": 0,
+ "in_create": 0,
+ "is_submittable": 0,
+ "issingle": 0,
+ "istable": 0,
+ "max_attachments": 0,
+ "modified": "2018-02-26 04:40:16.510290",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Subscriber",
+ "name_case": "",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "amend": 0,
+ "apply_user_permissions": 0,
+ "cancel": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "if_owner": 0,
+ "import": 0,
+ "permlevel": 0,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "set_user_permissions": 0,
+ "share": 1,
+ "submit": 0,
+ "write": 1
+ }
+ ],
+ "quick_entry": 1,
+ "read_only": 0,
+ "read_only_onload": 0,
+ "show_name_in_global_search": 0,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1,
+ "track_seen": 0
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/subscriber/subscriber.py b/erpnext/accounts/doctype/subscriber/subscriber.py
new file mode 100644
index 0000000..c0aabcf
--- /dev/null
+++ b/erpnext/accounts/doctype/subscriber/subscriber.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe.model.document import Document
+
+class Subscriber(Document):
+ pass
diff --git a/erpnext/accounts/doctype/subscriber/test_subscriber.js b/erpnext/accounts/doctype/subscriber/test_subscriber.js
new file mode 100644
index 0000000..1fd4a1e
--- /dev/null
+++ b/erpnext/accounts/doctype/subscriber/test_subscriber.js
@@ -0,0 +1,23 @@
+/* eslint-disable */
+// rename this file from _test_[name] to test_[name] to activate
+// and remove above this line
+
+QUnit.test("test: Subscriber", function (assert) {
+ let done = assert.async();
+
+ // number of asserts
+ assert.expect(1);
+
+ frappe.run_serially([
+ // insert a new Subscriber
+ () => frappe.tests.make('Subscriber', [
+ // values to be set
+ {key: 'value'}
+ ]),
+ () => {
+ assert.equal(cur_frm.doc.key, 'value');
+ },
+ () => done()
+ ]);
+
+});
diff --git a/erpnext/accounts/doctype/subscriber/test_subscriber.py b/erpnext/accounts/doctype/subscriber/test_subscriber.py
new file mode 100644
index 0000000..e8684c3
--- /dev/null
+++ b/erpnext/accounts/doctype/subscriber/test_subscriber.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import frappe
+import unittest
+
+class TestSubscriber(unittest.TestCase):
+ pass
diff --git a/erpnext/accounts/doctype/subscription_invoice/__init__.py b/erpnext/accounts/doctype/subscription_invoice/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/subscription_invoice/__init__.py
diff --git a/erpnext/accounts/doctype/subscription_invoice/subscription_invoice.js b/erpnext/accounts/doctype/subscription_invoice/subscription_invoice.js
new file mode 100644
index 0000000..40f9af3
--- /dev/null
+++ b/erpnext/accounts/doctype/subscription_invoice/subscription_invoice.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Subscription Invoice', {
+ refresh: function(frm) {
+
+ }
+});
diff --git a/erpnext/accounts/doctype/subscription_invoice/subscription_invoice.json b/erpnext/accounts/doctype/subscription_invoice/subscription_invoice.json
new file mode 100644
index 0000000..c4bae1d
--- /dev/null
+++ b/erpnext/accounts/doctype/subscription_invoice/subscription_invoice.json
@@ -0,0 +1,73 @@
+{
+ "allow_copy": 0,
+ "allow_guest_to_view": 0,
+ "allow_import": 0,
+ "allow_rename": 0,
+ "beta": 0,
+ "creation": "2018-02-26 04:21:41.265055",
+ "custom": 0,
+ "docstatus": 0,
+ "doctype": "DocType",
+ "document_type": "",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "fields": [
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "invoice",
+ "fieldtype": "Link",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 1,
+ "in_standard_filter": 0,
+ "label": "Invoice",
+ "length": 0,
+ "no_copy": 0,
+ "options": "Sales Invoice",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 1,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ }
+ ],
+ "has_web_view": 0,
+ "hide_heading": 0,
+ "hide_toolbar": 0,
+ "idx": 0,
+ "image_view": 0,
+ "in_create": 0,
+ "is_submittable": 0,
+ "issingle": 0,
+ "istable": 1,
+ "max_attachments": 0,
+ "modified": "2018-02-26 10:48:07.033422",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Subscription Invoice",
+ "name_case": "",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "read_only": 0,
+ "read_only_onload": 0,
+ "show_name_in_global_search": 0,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1,
+ "track_seen": 0
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/subscription_invoice/subscription_invoice.py b/erpnext/accounts/doctype/subscription_invoice/subscription_invoice.py
new file mode 100644
index 0000000..69ff3e5
--- /dev/null
+++ b/erpnext/accounts/doctype/subscription_invoice/subscription_invoice.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe.model.document import Document
+
+class SubscriptionInvoice(Document):
+ pass
diff --git a/erpnext/accounts/doctype/subscription_invoice/test_subscription_invoice.js b/erpnext/accounts/doctype/subscription_invoice/test_subscription_invoice.js
new file mode 100644
index 0000000..15d3df2
--- /dev/null
+++ b/erpnext/accounts/doctype/subscription_invoice/test_subscription_invoice.js
@@ -0,0 +1,23 @@
+/* eslint-disable */
+// rename this file from _test_[name] to test_[name] to activate
+// and remove above this line
+
+QUnit.test("test: Subscription Invoice", function (assert) {
+ let done = assert.async();
+
+ // number of asserts
+ assert.expect(1);
+
+ frappe.run_serially([
+ // insert a new Subscription Invoice
+ () => frappe.tests.make('Subscription Invoice', [
+ // values to be set
+ {key: 'value'}
+ ]),
+ () => {
+ assert.equal(cur_frm.doc.key, 'value');
+ },
+ () => done()
+ ]);
+
+});
diff --git a/erpnext/accounts/doctype/subscription_invoice/test_subscription_invoice.py b/erpnext/accounts/doctype/subscription_invoice/test_subscription_invoice.py
new file mode 100644
index 0000000..1d542b0
--- /dev/null
+++ b/erpnext/accounts/doctype/subscription_invoice/test_subscription_invoice.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import frappe
+import unittest
+
+class TestSubscriptionInvoice(unittest.TestCase):
+ pass
diff --git a/erpnext/accounts/doctype/subscription_plan/__init__.py b/erpnext/accounts/doctype/subscription_plan/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/subscription_plan/__init__.py
diff --git a/erpnext/accounts/doctype/subscription_plan/subscription_plan.js b/erpnext/accounts/doctype/subscription_plan/subscription_plan.js
new file mode 100644
index 0000000..9baacdd
--- /dev/null
+++ b/erpnext/accounts/doctype/subscription_plan/subscription_plan.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Subscription Plan', {
+ refresh: function(frm) {
+
+ }
+});
diff --git a/erpnext/accounts/doctype/subscription_plan/subscription_plan.json b/erpnext/accounts/doctype/subscription_plan/subscription_plan.json
new file mode 100644
index 0000000..ab58e7c
--- /dev/null
+++ b/erpnext/accounts/doctype/subscription_plan/subscription_plan.json
@@ -0,0 +1,255 @@
+{
+ "allow_copy": 0,
+ "allow_guest_to_view": 0,
+ "allow_import": 0,
+ "allow_rename": 1,
+ "autoname": "field:plan_name",
+ "beta": 0,
+ "creation": "2018-02-24 11:31:23.066506",
+ "custom": 0,
+ "docstatus": 0,
+ "doctype": "DocType",
+ "document_type": "",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "fields": [
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "plan_name",
+ "fieldtype": "Data",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 1,
+ "in_standard_filter": 0,
+ "label": "Plan Name",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 1,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "item",
+ "fieldtype": "Link",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 1,
+ "in_standard_filter": 0,
+ "label": "Item",
+ "length": 0,
+ "no_copy": 0,
+ "options": "Item",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 1,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "currency",
+ "fieldtype": "Link",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Currency",
+ "length": 0,
+ "no_copy": 0,
+ "options": "Currency",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "cost",
+ "fieldtype": "Currency",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 1,
+ "in_standard_filter": 0,
+ "label": "Cost",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 1,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "default": "Day",
+ "fieldname": "billing_interval",
+ "fieldtype": "Select",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 1,
+ "in_standard_filter": 0,
+ "label": "Billing Interval",
+ "length": 0,
+ "no_copy": 0,
+ "options": "Day\nWeek\nMonth\nYear",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 1,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "default": "1",
+ "description": "Number of intervals for the interval field e.g if Interval is 'Days' and Billing Interval Count is 3, invoices will be generated every 3 days",
+ "fieldname": "billing_interval_count",
+ "fieldtype": "Int",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Billing Interval Count",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 1,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ }
+ ],
+ "has_web_view": 0,
+ "hide_heading": 0,
+ "hide_toolbar": 0,
+ "idx": 0,
+ "image_view": 0,
+ "in_create": 0,
+ "is_submittable": 0,
+ "issingle": 0,
+ "istable": 0,
+ "max_attachments": 0,
+ "modified": "2018-02-27 09:12:58.330140",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Subscription Plan",
+ "name_case": "",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "amend": 0,
+ "apply_user_permissions": 0,
+ "cancel": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "if_owner": 0,
+ "import": 0,
+ "permlevel": 0,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "set_user_permissions": 0,
+ "share": 1,
+ "submit": 0,
+ "write": 1
+ }
+ ],
+ "quick_entry": 1,
+ "read_only": 0,
+ "read_only_onload": 0,
+ "show_name_in_global_search": 0,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1,
+ "track_seen": 0
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/subscription_plan/subscription_plan.py b/erpnext/accounts/doctype/subscription_plan/subscription_plan.py
new file mode 100644
index 0000000..4b8c8fc
--- /dev/null
+++ b/erpnext/accounts/doctype/subscription_plan/subscription_plan.py
@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe.model.document import Document
+
+class SubscriptionPlan(Document):
+ def validate(self):
+ self.validate_interval_count()
+
+ def validate_interval_count(self):
+ if self.billing_interval_count < 1:
+ frappe.throw('Billing Interval Count cannot be less than 1')
diff --git a/erpnext/accounts/doctype/subscription_plan/test_subscription_plan.js b/erpnext/accounts/doctype/subscription_plan/test_subscription_plan.js
new file mode 100644
index 0000000..3ceb9a6
--- /dev/null
+++ b/erpnext/accounts/doctype/subscription_plan/test_subscription_plan.js
@@ -0,0 +1,23 @@
+/* eslint-disable */
+// rename this file from _test_[name] to test_[name] to activate
+// and remove above this line
+
+QUnit.test("test: Subscription Plan", function (assert) {
+ let done = assert.async();
+
+ // number of asserts
+ assert.expect(1);
+
+ frappe.run_serially([
+ // insert a new Subscription Plan
+ () => frappe.tests.make('Subscription Plan', [
+ // values to be set
+ {key: 'value'}
+ ]),
+ () => {
+ assert.equal(cur_frm.doc.key, 'value');
+ },
+ () => done()
+ ]);
+
+});
diff --git a/erpnext/accounts/doctype/subscription_plan/test_subscription_plan.py b/erpnext/accounts/doctype/subscription_plan/test_subscription_plan.py
new file mode 100644
index 0000000..4a9b578
--- /dev/null
+++ b/erpnext/accounts/doctype/subscription_plan/test_subscription_plan.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import frappe
+import unittest
+
+class TestSubscriptionPlan(unittest.TestCase):
+ pass
diff --git a/erpnext/accounts/doctype/subscription_plan_detail/__init__.py b/erpnext/accounts/doctype/subscription_plan_detail/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/subscription_plan_detail/__init__.py
diff --git a/erpnext/accounts/doctype/subscription_plan_detail/subscription_plan_detail.json b/erpnext/accounts/doctype/subscription_plan_detail/subscription_plan_detail.json
new file mode 100644
index 0000000..c112923
--- /dev/null
+++ b/erpnext/accounts/doctype/subscription_plan_detail/subscription_plan_detail.json
@@ -0,0 +1,73 @@
+{
+ "allow_copy": 0,
+ "allow_guest_to_view": 0,
+ "allow_import": 0,
+ "allow_rename": 0,
+ "beta": 0,
+ "creation": "2018-02-25 07:35:07.736146",
+ "custom": 0,
+ "docstatus": 0,
+ "doctype": "DocType",
+ "document_type": "",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "fields": [
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "plan",
+ "fieldtype": "Link",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 1,
+ "in_standard_filter": 0,
+ "label": "Plan",
+ "length": 0,
+ "no_copy": 0,
+ "options": "Subscription Plan",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 1,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ }
+ ],
+ "has_web_view": 0,
+ "hide_heading": 0,
+ "hide_toolbar": 0,
+ "idx": 0,
+ "image_view": 0,
+ "in_create": 0,
+ "is_submittable": 0,
+ "issingle": 0,
+ "istable": 1,
+ "max_attachments": 0,
+ "modified": "2018-02-25 07:35:07.736146",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Subscription Plan Detail",
+ "name_case": "",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "read_only": 0,
+ "read_only_onload": 0,
+ "show_name_in_global_search": 0,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1,
+ "track_seen": 0
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/subscription_plan_detail/subscription_plan_detail.py b/erpnext/accounts/doctype/subscription_plan_detail/subscription_plan_detail.py
new file mode 100644
index 0000000..04ec4af
--- /dev/null
+++ b/erpnext/accounts/doctype/subscription_plan_detail/subscription_plan_detail.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe.model.document import Document
+
+class SubscriptionPlanDetail(Document):
+ pass
diff --git a/erpnext/accounts/doctype/subscription_settings/__init__.py b/erpnext/accounts/doctype/subscription_settings/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/subscription_settings/__init__.py
diff --git a/erpnext/accounts/doctype/subscription_settings/subscription_settings.js b/erpnext/accounts/doctype/subscription_settings/subscription_settings.js
new file mode 100644
index 0000000..c4541c3
--- /dev/null
+++ b/erpnext/accounts/doctype/subscription_settings/subscription_settings.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Subscription Settings', {
+ refresh: function(frm) {
+
+ }
+});
diff --git a/erpnext/accounts/doctype/subscription_settings/subscription_settings.json b/erpnext/accounts/doctype/subscription_settings/subscription_settings.json
new file mode 100644
index 0000000..8c7c6f3
--- /dev/null
+++ b/erpnext/accounts/doctype/subscription_settings/subscription_settings.json
@@ -0,0 +1,179 @@
+{
+ "allow_copy": 0,
+ "allow_guest_to_view": 0,
+ "allow_import": 0,
+ "allow_rename": 0,
+ "beta": 0,
+ "creation": "2018-02-26 06:13:37.910139",
+ "custom": 0,
+ "docstatus": 0,
+ "doctype": "DocType",
+ "document_type": "",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "fields": [
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "default": "1",
+ "description": "Number of days after invoice date has elapsed before canceling subscription or marking subscription as unpaid",
+ "fieldname": "grace_period",
+ "fieldtype": "Int",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Grace Period",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "default": "0",
+ "fieldname": "cancel_after_grace",
+ "fieldtype": "Check",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Cancel Invoice After Grace Period",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "default": "1",
+ "fieldname": "prorate",
+ "fieldtype": "Check",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Prorate",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ }
+ ],
+ "has_web_view": 0,
+ "hide_heading": 0,
+ "hide_toolbar": 0,
+ "idx": 0,
+ "image_view": 0,
+ "in_create": 0,
+ "is_submittable": 0,
+ "issingle": 1,
+ "istable": 0,
+ "max_attachments": 0,
+ "modified": "2018-02-26 13:58:09.455832",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Subscription Settings",
+ "name_case": "",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "amend": 0,
+ "apply_user_permissions": 0,
+ "cancel": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 0,
+ "if_owner": 0,
+ "import": 0,
+ "permlevel": 0,
+ "print": 1,
+ "read": 1,
+ "report": 0,
+ "role": "System Manager",
+ "set_user_permissions": 0,
+ "share": 1,
+ "submit": 0,
+ "write": 1
+ },
+ {
+ "amend": 0,
+ "apply_user_permissions": 0,
+ "cancel": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 0,
+ "if_owner": 0,
+ "import": 0,
+ "permlevel": 0,
+ "print": 1,
+ "read": 1,
+ "report": 0,
+ "role": "Administrator",
+ "set_user_permissions": 0,
+ "share": 1,
+ "submit": 0,
+ "write": 1
+ }
+ ],
+ "quick_entry": 1,
+ "read_only": 0,
+ "read_only_onload": 0,
+ "show_name_in_global_search": 0,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1,
+ "track_seen": 0
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/subscription_settings/subscription_settings.py b/erpnext/accounts/doctype/subscription_settings/subscription_settings.py
new file mode 100644
index 0000000..3d382a7
--- /dev/null
+++ b/erpnext/accounts/doctype/subscription_settings/subscription_settings.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe.model.document import Document
+
+class SubscriptionSettings(Document):
+ pass
diff --git a/erpnext/accounts/doctype/subscription_settings/test_subscription_settings.js b/erpnext/accounts/doctype/subscription_settings/test_subscription_settings.js
new file mode 100644
index 0000000..5a751ea
--- /dev/null
+++ b/erpnext/accounts/doctype/subscription_settings/test_subscription_settings.js
@@ -0,0 +1,23 @@
+/* eslint-disable */
+// rename this file from _test_[name] to test_[name] to activate
+// and remove above this line
+
+QUnit.test("test: Subscription Settings", function (assert) {
+ let done = assert.async();
+
+ // number of asserts
+ assert.expect(1);
+
+ frappe.run_serially([
+ // insert a new Subscription Settings
+ () => frappe.tests.make('Subscription Settings', [
+ // values to be set
+ {key: 'value'}
+ ]),
+ () => {
+ assert.equal(cur_frm.doc.key, 'value');
+ },
+ () => done()
+ ]);
+
+});
diff --git a/erpnext/accounts/doctype/subscription_settings/test_subscription_settings.py b/erpnext/accounts/doctype/subscription_settings/test_subscription_settings.py
new file mode 100644
index 0000000..b9592d3
--- /dev/null
+++ b/erpnext/accounts/doctype/subscription_settings/test_subscription_settings.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import frappe
+import unittest
+
+class TestSubscriptionSettings(unittest.TestCase):
+ pass
diff --git a/erpnext/accounts/doctype/subscriptions/__init__.py b/erpnext/accounts/doctype/subscriptions/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/accounts/doctype/subscriptions/__init__.py
diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.js b/erpnext/accounts/doctype/subscriptions/subscriptions.js
new file mode 100644
index 0000000..92cb93f
--- /dev/null
+++ b/erpnext/accounts/doctype/subscriptions/subscriptions.js
@@ -0,0 +1,78 @@
+// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Subscriptions', {
+ refresh: function(frm) {
+ if(!frm.is_new()){
+ if(frm.doc.status !== 'Canceled'){
+ frm.add_custom_button(
+ __('Cancel Subscription'),
+ () => frm.events.cancel_this_subscription(frm)
+ );
+ frm.add_custom_button(
+ __('Fetch Subscription Updates'),
+ () => frm.events.get_subscription_updates(frm)
+ );
+ }
+ else if(frm.doc.status === 'Canceled'){
+ frm.add_custom_button(
+ __('Restart Subscription'),
+ () => frm.events.renew_this_subscription(frm)
+ );
+ }
+ }
+ },
+
+ cancel_this_subscription: function(frm) {
+ const doc = frm.doc;
+ frappe.confirm(
+ __('This action will stop future billing. Are you sure you want to cancel this subscription?'),
+ function() {
+ frappe.call({
+ method:
+ "erpnext.accounts.doctype.subscriptions.subscriptions.cancel_subscription",
+ args: {name: doc.name},
+ callback: function(data){
+ if(!data.exc){
+ frm.reload_doc();
+ }
+ }
+ });
+ }
+ );
+ },
+
+ renew_this_subscription: function(frm) {
+ const doc = frm.doc;
+ frappe.confirm(
+ __('You will lose records of previously generated invoices. Are you sure you want to restart this subscription?'),
+ function() {
+ frappe.call({
+ method:
+ "erpnext.accounts.doctype.subscriptions.subscriptions.restart_subscription",
+ args: {name: doc.name},
+ callback: function(data){
+ if(!data.exc){
+ frm.reload_doc();
+ }
+ }
+ });
+ }
+ );
+ },
+
+ get_subscription_updates: function(frm) {
+ const doc = frm.doc;
+ frappe.call({
+ method:
+ "erpnext.accounts.doctype.subscriptions.subscriptions.get_subscription_updates",
+ args: {name: doc.name},
+ freeze: true,
+ callback: function(data){
+ if(!data.exc){
+ frm.reload_doc();
+ }
+ }
+ });
+ }
+});
diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.json b/erpnext/accounts/doctype/subscriptions/subscriptions.json
new file mode 100644
index 0000000..90949e3
--- /dev/null
+++ b/erpnext/accounts/doctype/subscriptions/subscriptions.json
@@ -0,0 +1,695 @@
+{
+ "allow_copy": 0,
+ "allow_guest_to_view": 0,
+ "allow_import": 0,
+ "allow_rename": 0,
+ "autoname": "SUB.####",
+ "beta": 0,
+ "creation": "2018-02-26 04:13:14.153718",
+ "custom": 0,
+ "docstatus": 0,
+ "doctype": "DocType",
+ "document_type": "",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "fields": [
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "subscriber",
+ "fieldtype": "Link",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 1,
+ "in_standard_filter": 0,
+ "label": "Subscriber",
+ "length": 0,
+ "no_copy": 0,
+ "options": "Subscriber",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 1,
+ "search_index": 0,
+ "set_only_once": 1,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "default": "0",
+ "fieldname": "cancel_at_period_end",
+ "fieldtype": "Check",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Cancel At End Of Period",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "start",
+ "fieldtype": "Date",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Subscription Start Date",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 1,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "cancelation_date",
+ "fieldtype": "Date",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Cancelation Date",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 1,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "current_invoice_start",
+ "fieldtype": "Date",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Current Invoice Start Date",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 1,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "current_invoice_end",
+ "fieldtype": "Date",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Current Invoice End Date",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 1,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "trial_period_start",
+ "fieldtype": "Date",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Trial Period Start Date",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 1,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "depends_on": "eval:doc.trial_period_start",
+ "fieldname": "trial_period_end",
+ "fieldtype": "Date",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Trial Period End Date",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 1,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "default": "0",
+ "description": "Number of days that the subscriber has to pay invoices generated by this subscription",
+ "fieldname": "days_until_due",
+ "fieldtype": "Int",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Days Until Due",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "default": "1",
+ "fieldname": "quantity",
+ "fieldtype": "Int",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Quantity",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "plans",
+ "fieldtype": "Table",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Plans",
+ "length": 0,
+ "no_copy": 0,
+ "options": "Subscription Plan Detail",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 1,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "sb_1",
+ "fieldtype": "Section Break",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Taxes",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "tax_template",
+ "fieldtype": "Link",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Sales Taxes and Charges Template",
+ "length": 0,
+ "no_copy": 0,
+ "options": "Sales Taxes and Charges Template",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "sb_2",
+ "fieldtype": "Section Break",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Discounts",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "apply_additional_discount",
+ "fieldtype": "Select",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Apply Additional Discount On",
+ "length": 0,
+ "no_copy": 0,
+ "options": "\nGrand Total\nNet total",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "additional_discount_percentage",
+ "fieldtype": "Percent",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Additional DIscount Percentage",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "additional_discount_amount",
+ "fieldtype": "Currency",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Additional DIscount Amount",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Status",
+ "length": 0,
+ "no_copy": 0,
+ "options": "Trialling\nActive\nPast Due Date\nCanceled\nUnpaid",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 1,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "depends_on": "eval:doc.invoices",
+ "fieldname": "sb_3",
+ "fieldtype": "Section Break",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Invoices",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "invoices",
+ "fieldtype": "Table",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Invoices",
+ "length": 0,
+ "no_copy": 0,
+ "options": "Subscription Invoice",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ }
+ ],
+ "has_web_view": 0,
+ "hide_heading": 0,
+ "hide_toolbar": 0,
+ "idx": 0,
+ "image_view": 0,
+ "in_create": 0,
+ "is_submittable": 0,
+ "issingle": 0,
+ "istable": 0,
+ "max_attachments": 0,
+ "modified": "2018-03-01 17:12:11.105074",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Subscriptions",
+ "name_case": "",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "amend": 0,
+ "apply_user_permissions": 0,
+ "cancel": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "if_owner": 0,
+ "import": 0,
+ "permlevel": 0,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "set_user_permissions": 0,
+ "share": 1,
+ "submit": 0,
+ "write": 1
+ }
+ ],
+ "quick_entry": 0,
+ "read_only": 0,
+ "read_only_onload": 0,
+ "show_name_in_global_search": 0,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1,
+ "track_seen": 0
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.py b/erpnext/accounts/doctype/subscriptions/subscriptions.py
new file mode 100644
index 0000000..2fd2fd6
--- /dev/null
+++ b/erpnext/accounts/doctype/subscriptions/subscriptions.py
@@ -0,0 +1,491 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+
+import frappe
+from frappe import _
+from frappe.model.document import Document
+from frappe.utils.data import nowdate, getdate, cint, add_days, date_diff, get_last_day, add_to_date
+
+
+class Subscriptions(Document):
+ def before_insert(self):
+ # update start just before the subscription doc is created
+ self.update_subscription_period(self.start)
+
+ def update_subscription_period(self, date=None):
+ """
+ Subscription period is the period to be billed. This method updates the
+ beginning of the billing period and end of the billing period.
+
+ The beginning of the billing period is represented in the doctype as
+ `current_invoice_start` and the end of the billing period is represented
+ as `current_invoice_end`.
+ """
+ self.set_current_invoice_start(date)
+ self.set_current_invoice_end()
+
+ def set_current_invoice_start(self, date=None):
+ """
+ This sets the date of the beginning of the current billing period.
+
+ If the `date` parameter is not given , it will be automatically set as today's
+ date.
+ """
+ if self.trial_period_start and self.is_trialling():
+ self.current_invoice_start = self.trial_period_start
+ elif not date:
+ self.current_invoice_start = nowdate()
+ elif date:
+ self.current_invoice_start = date
+
+ def set_current_invoice_end(self):
+ """
+ This sets the date of the end of the current billing period.
+
+ If the subscription is in trial period, it will be set as the end of the
+ trial period.
+
+ If is not in a trial period, it will be `x` days from the beginning of the
+ current billing period where `x` is the billing interval from the
+ `Subscription Plan` in the `Subscription`.
+ """
+ if self.is_trialling():
+ self.current_invoice_end = self.trial_period_end
+ else:
+ billing_cycle_info = self.get_billing_cycle()
+ if billing_cycle_info:
+ self.current_invoice_end = add_to_date(self.current_invoice_start, **billing_cycle_info)
+ else:
+ self.current_invoice_end = get_last_day(self.current_invoice_start)
+
+ def get_billing_cycle(self):
+ """
+ Returns a dict containing billing cycle information deduced from the
+ `Subscription Plan` in the `Subscription`.
+ """
+ return self.get_billing_cycle_data()
+
+ @staticmethod
+ def validate_plans_billing_cycle(billing_cycle_data):
+ """
+ Makes sure that all `Subscription Plan` in the `Subscription` have the
+ same billing interval
+ """
+ if billing_cycle_data and len(billing_cycle_data) != 1:
+ frappe.throw(_('You can only have Plans with the same billing cycle in a Subscription'))
+
+ def get_billing_cycle_and_interval(self):
+ """
+ Returns a dict representing the billing interval and cycle for this `Subscription`.
+
+ You shouldn't need to call this directly. Use `get_billing_cycle` instead.
+ """
+ plan_names = [plan.plan for plan in self.plans]
+ billing_info = frappe.db.sql(
+ 'select distinct `billing_interval`, `billing_interval_count` '
+ 'from `tabSubscription Plan` '
+ 'where name in %s',
+ (plan_names,), as_dict=1
+ )
+
+ return billing_info
+
+ def get_billing_cycle_data(self):
+ """
+ Returns dict contain the billing cycle data.
+
+ You shouldn't need to call this directly. Use `get_billing_cycle` instead.
+ """
+ billing_info = self.get_billing_cycle_and_interval()
+
+ self.validate_plans_billing_cycle(billing_info)
+
+ if billing_info:
+ data = dict()
+ interval = billing_info[0]['billing_interval']
+ interval_count = billing_info[0]['billing_interval_count']
+ if interval not in ['Day', 'Week']:
+ data['days'] = -1
+ if interval == 'Day':
+ data['days'] = interval_count - 1
+ elif interval == 'Month':
+ data['months'] = interval_count
+ elif interval == 'Year':
+ data['years'] = interval_count
+ # todo: test week
+ elif interval == 'Week':
+ data['days'] = interval_count * 7 - 1
+
+ return data
+
+ def set_status_grace_period(self):
+ """
+ Sets the `Subscription` `status` based on the preference set in `Subscription Settings`.
+
+ Used when the `Subscription` needs to decide what to do after the current generated
+ invoice is past it's due date and grace period.
+ """
+ subscription_settings = frappe.get_single('Subscription Settings')
+ if self.status == 'Past Due Date' and self.is_past_grace_period():
+ self.status = 'Canceled' if cint(subscription_settings.cancel_after_grace) else 'Unpaid'
+
+ def set_subscription_status(self):
+ """
+ Sets the status of the `Subscription`
+ """
+ if self.is_trialling():
+ self.status = 'Trialling'
+ elif self.status == 'Past Due Date' and self.is_past_grace_period():
+ subscription_settings = frappe.get_single('Subscription Settings')
+ self.status = 'Canceled' if cint(subscription_settings.cancel_after_grace) else 'Unpaid'
+ elif self.status == 'Past Due Date' and not self.has_outstanding_invoice():
+ self.status = 'Active'
+ elif self.current_invoice_is_past_due():
+ self.status = 'Past Due Date'
+ elif self.is_new_subscription():
+ self.status = 'Active'
+ # todo: then generate new invoice
+ self.save()
+
+ def is_trialling(self):
+ """
+ Returns `True` if the `Subscription` is trial period.
+ """
+ return not self.period_has_passed(self.trial_period_end) and self.is_new_subscription()
+
+ @staticmethod
+ def period_has_passed(end_date):
+ """
+ Returns true if the given `end_date` has passed
+ """
+ # todo: test for illegal time
+ if not end_date:
+ return True
+
+ end_date = getdate(end_date)
+ return getdate(nowdate()) > getdate(end_date)
+
+ def is_past_grace_period(self):
+ """
+ Returns `True` if the grace period for the `Subscription` has passed
+ """
+ current_invoice = self.get_current_invoice()
+ if self.current_invoice_is_past_due(current_invoice):
+ subscription_settings = frappe.get_single('Subscription Settings')
+ grace_period = cint(subscription_settings.grace_period)
+
+ return getdate(nowdate()) > add_days(current_invoice.due_date, grace_period)
+
+ def current_invoice_is_past_due(self, current_invoice=None):
+ """
+ Returns `True` if the current generated invoice is overdue
+ """
+ if not current_invoice:
+ current_invoice = self.get_current_invoice()
+
+ if not current_invoice:
+ return False
+ else:
+ return getdate(nowdate()) > getdate(current_invoice.due_date)
+
+ def get_current_invoice(self):
+ """
+ Returns the most recent generated invoice.
+ """
+ if len(self.invoices):
+ current = self.invoices[-1]
+ if frappe.db.exists('Sales Invoice', current.invoice):
+ doc = frappe.get_doc('Sales Invoice', current.invoice)
+ return doc
+ else:
+ frappe.throw(_('Invoice {0} no longer exists'.format(current.invoice)))
+
+ def is_new_subscription(self):
+ """
+ Returns `True` if `Subscription` has never generated an invoice
+ """
+ return len(self.invoices) == 0
+
+ def validate(self):
+ self.validate_trial_period()
+ self.validate_plans_billing_cycle(self.get_billing_cycle_and_interval())
+
+ def validate_trial_period(self):
+ """
+ Runs sanity checks on trial period dates for the `Subscription`
+ """
+ if self.trial_period_start and self.trial_period_end:
+ if getdate(self.trial_period_end) < getdate(self.trial_period_start):
+ frappe.throw(_('Trial Period End Date Cannot be before Trial Period Start Date'))
+
+ elif self.trial_period_start or self.trial_period_end:
+ frappe.throw(_('Both Trial Period Start Date and Trial Period End Date must be set'))
+
+ def after_insert(self):
+ # todo: deal with users who collect prepayments. Maybe a new Subscription Invoice doctype?
+ self.set_subscription_status()
+
+ def generate_invoice(self, prorate=0):
+ """
+ Creates a `Sales Invoice` for the `Subscription`, updates `self.invoices` and
+ saves the `Subscription`.
+ """
+ invoice = self.create_invoice(prorate)
+ self.append('invoices', {'invoice': invoice.name})
+ self.save()
+
+ return invoice
+
+ def create_invoice(self, prorate):
+ """
+ Creates a `Sales Invoice`, submits it and returns it
+ """
+ invoice = frappe.new_doc('Sales Invoice')
+ invoice.set_posting_time = 1
+ invoice.posting_date = self.current_invoice_start
+ invoice.customer = self.get_customer(self.subscriber)
+
+ # Subscription is better suited for service items. I won't update `update_stock`
+ # for that reason
+ items_list = self.get_items_from_plans(self.plans, prorate)
+ for item in items_list:
+ item['qty'] = self.quantity
+ invoice.append('items', item)
+
+ # Taxes
+ if self.tax_template:
+ invoice.taxes_and_charges = self.tax_template
+ invoice.set_taxes()
+
+ # Due date
+ invoice.append(
+ 'payment_schedule',
+ {
+ 'due_date': add_days(self.current_invoice_end, cint(self.days_until_due)),
+ 'invoice_portion': 100
+ }
+ )
+
+ # Discounts
+ if self.additional_discount_percentage:
+ invoice.additional_discount_percentage = self.additional_discount_percentage
+
+ if self.additional_discount_amount:
+ invoice.additional_discount_amount = self.additional_discount_amount
+
+ if (self.additional_discount_percentage or self.additional_discount_amount) \
+ and not self.apply_additional_discount:
+ self.apply_additional_discount = 'Grand Total'
+
+ invoice.save()
+ invoice.submit()
+
+ return invoice
+
+ @staticmethod
+ def get_customer(subscriber_name):
+ """
+ Returns the `Customer` linked to the `Subscriber`
+ """
+ return frappe.get_value('Subscriber', subscriber_name)
+
+ def get_items_from_plans(self, plans, prorate=0):
+ """
+ Returns the `Item`s linked to `Subscription Plan`
+ """
+ plan_items = [plan.plan for plan in plans]
+ item_names = None
+
+ if plan_items and not prorate:
+ item_names = frappe.db.sql(
+ 'select item as item_code, cost as rate from `tabSubscription Plan` where name in %s',
+ (plan_items,), as_dict=1
+ )
+
+ elif plan_items:
+ prorate_factor = get_prorata_factor(self.current_invoice_end, self.current_invoice_start)
+
+ item_names = frappe.db.sql(
+ 'select item as item_code, cost * %s as rate from `tabSubscription Plan` where name in %s',
+ (prorate_factor, plan_items,), as_dict=1
+ )
+
+ return item_names
+
+ def process(self):
+ """
+ To be called by task periodically. It checks the subscription and takes appropriate action
+ as need be. It calls either of these methods depending the `Subscription` status:
+ 1. `process_for_active`
+ 2. `process_for_past_due`
+ """
+ if self.status == 'Active':
+ self.process_for_active()
+ elif self.status in ['Past Due Date', 'Unpaid']:
+ self.process_for_past_due_date()
+
+ self.save()
+
+ def process_for_active(self):
+ """
+ Called by `process` if the status of the `Subscription` is 'Active'.
+
+ The possible outcomes of this method are:
+ 1. Generate a new invoice
+ 2. Change the `Subscription` status to 'Past Due Date'
+ 3. Change the `Subscription` status to 'Canceled'
+ """
+ if getdate(nowdate()) > getdate(self.current_invoice_end) and not self.has_outstanding_invoice():
+ self.generate_invoice()
+ if self.current_invoice_is_past_due():
+ self.status = 'Past Due Date'
+
+ if self.current_invoice_is_past_due() and getdate(nowdate()) > getdate(self.current_invoice_end):
+ self.status = 'Past Due Date'
+
+ if self.cancel_at_period_end and getdate(nowdate()) > self.current_invoice_end:
+ self.cancel_subscription_at_period_end()
+
+ def cancel_subscription_at_period_end(self):
+ """
+ Called when `Subscription.cancel_at_period_end` is truthy
+ """
+ self.status = 'Canceled'
+ if not self.cancelation_date:
+ self.cancelation_date = nowdate()
+
+ def process_for_past_due_date(self):
+ """
+ Called by `process` if the status of the `Subscription` is 'Past Due Date'.
+
+ The possible outcomes of this method are:
+ 1. Change the `Subscription` status to 'Active'
+ 2. Change the `Subscription` status to 'Canceled'
+ 3. Change the `Subscription` status to 'Unpaid'
+ """
+ current_invoice = self.get_current_invoice()
+ if not current_invoice:
+ frappe.throw(_('Current invoice {0} is missing'.format(current_invoice.invoice)))
+ else:
+ if self.is_not_outstanding(current_invoice):
+ self.status = 'Active'
+ self.update_subscription_period(nowdate())
+ else:
+ self.set_status_grace_period()
+
+ @staticmethod
+ def is_not_outstanding(invoice):
+ """
+ Return `True` if the given invoice is paid
+ """
+ return invoice.status == 'Paid'
+
+ def has_outstanding_invoice(self):
+ """
+ Returns `True` if the most recent invoice for the `Subscription` is not paid
+ """
+ current_invoice = self.get_current_invoice()
+ if not current_invoice:
+ return False
+ else:
+ return not self.is_not_outstanding(current_invoice)
+
+ def cancel_subscription(self):
+ """
+ This sets the subscription as cancelled. It will stop invoices from being generated
+ but it will not affect already created invoices.
+ """
+ to_generate_invoice = True if self.status == 'Active' else False
+ to_prorate = frappe.db.get_single_value('Subscription Settings', 'prorate')
+ self.status = 'Canceled'
+ self.cancelation_date = nowdate()
+ if to_generate_invoice:
+ self.generate_invoice(prorate=to_prorate)
+ self.save()
+
+ def restart_subscription(self):
+ """
+ This sets the subscription as active. The subscription will be made to be like a new
+ subscription and the `Subscription` will lose all the history of generated invoices
+ it has.
+ """
+ self.status = 'Active'
+ self.db_set('start', nowdate())
+ self.update_subscription_period(nowdate())
+ self.invoices = []
+ self.save()
+
+
+def process_all():
+ """
+ Task to updates the status of all `Subscription` apart from those that are cancelled
+ """
+ subscriptions = get_all_subscriptions()
+ for subscription in subscriptions:
+ process(subscription)
+
+
+def get_all_subscriptions():
+ """
+ Returns all `Subscription` documents
+ """
+ return frappe.db.sql(
+ 'select name from `tabSubscriptions` where status != "Canceled"',
+ as_dict=1
+ )
+
+
+def process(data):
+ """
+ Checks a `Subscription` and updates it status as necessary
+ """
+ if data:
+ try:
+ subscription = frappe.get_doc('Subscriptions', data['name'])
+ subscription.process()
+ frappe.db.commit()
+ except frappe.ValidationError:
+ frappe.db.rollback()
+ frappe.db.begin()
+ frappe.log_error(frappe.get_traceback())
+ frappe.db.commit()
+
+
+def get_prorata_factor(period_end, period_start):
+ diff = date_diff(nowdate(), period_start) + 1
+ plan_days = date_diff(period_end, period_start) + 1
+ prorate_factor = diff/plan_days
+
+ return prorate_factor
+
+
+@frappe.whitelist()
+def cancel_subscription(name):
+ """
+ Cancels a `Subscription`. This will stop the `Subscription` from further invoicing the
+ `Subscriber` but all already outstanding invoices will not be affected.
+ """
+ subscription = frappe.get_doc('Subscriptions', name)
+ subscription.cancel_subscription()
+
+
+@frappe.whitelist()
+def restart_subscription(name):
+ """
+ Restarts a cancelled `Subscription`. The `Subscription` will 'forget' the history of
+ all invoices it has generated
+ """
+ subscription = frappe.get_doc('Subscriptions', name)
+ subscription.restart_subscription()
+
+
+@frappe.whitelist()
+def get_subscription_updates(name):
+ """
+ Use this to get the latest state of the given `Subscription`
+ """
+ subscription = frappe.get_doc('Subscriptions', name)
+ subscription.process()
diff --git a/erpnext/accounts/doctype/subscriptions/test_subscriptions.js b/erpnext/accounts/doctype/subscriptions/test_subscriptions.js
new file mode 100644
index 0000000..b5fe4ef
--- /dev/null
+++ b/erpnext/accounts/doctype/subscriptions/test_subscriptions.js
@@ -0,0 +1,23 @@
+/* eslint-disable */
+// rename this file from _test_[name] to test_[name] to activate
+// and remove above this line
+
+QUnit.test("test: Subscriptions", function (assert) {
+ let done = assert.async();
+
+ // number of asserts
+ assert.expect(1);
+
+ frappe.run_serially([
+ // insert a new Subscriptions
+ () => frappe.tests.make('Subscriptions', [
+ // values to be set
+ {key: 'value'}
+ ]),
+ () => {
+ assert.equal(cur_frm.doc.key, 'value');
+ },
+ () => done()
+ ]);
+
+});
diff --git a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py
new file mode 100644
index 0000000..627ebdd
--- /dev/null
+++ b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py
@@ -0,0 +1,446 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import unittest
+
+import frappe
+from erpnext.accounts.doctype.subscriptions.subscriptions import get_prorata_factor
+from frappe.utils.data import nowdate, add_days, add_to_date, add_months, date_diff
+
+
+class TestSubscriptions(unittest.TestCase):
+ def create_plan(self):
+ 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.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.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.cost = 1999
+ plan.billing_interval = 'Day'
+ plan.billing_interval_count = 14
+ plan.insert()
+
+ def create_subscriber(self):
+ if not frappe.db.exists('Subscriber', '_Test Customer'):
+ subscriber = frappe.new_doc('Subscriber')
+ subscriber.subscriber_name = '_Test Customer'
+ subscriber.customer = '_Test Customer'
+ subscriber.insert()
+
+ def setUp(self):
+ self.create_plan()
+ self.create_subscriber()
+
+ def test_create_subscription_with_trial_with_correct_period(self):
+ subscription = frappe.new_doc('Subscriptions')
+ subscription.subscriber = '_Test Customer'
+ subscription.trial_period_start = nowdate()
+ subscription.trial_period_end = add_days(nowdate(), 30)
+ subscription.append('plans', {'plan': '_Test Plan Name'})
+ subscription.save()
+
+ self.assertEqual(subscription.trial_period_start, nowdate())
+ self.assertEqual(subscription.trial_period_end, add_days(nowdate(), 30))
+ self.assertEqual(subscription.trial_period_start, subscription.current_invoice_start)
+ self.assertEqual(subscription.trial_period_end, subscription.current_invoice_end)
+ self.assertEqual(subscription.invoices, [])
+ self.assertEqual(subscription.status, 'Trialling')
+
+ subscription.delete()
+
+ def test_create_subscription_without_trial_with_correct_period(self):
+ subscription = frappe.new_doc('Subscriptions')
+ subscription.subscriber = '_Test Customer'
+ subscription.append('plans', {'plan': '_Test Plan Name'})
+ subscription.save()
+
+ self.assertEqual(subscription.trial_period_start, None)
+ self.assertEqual(subscription.trial_period_end, None)
+ self.assertEqual(subscription.current_invoice_start, nowdate())
+ self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1))
+ # No invoice is created
+ self.assertEqual(len(subscription.invoices), 0)
+ self.assertEqual(subscription.status, 'Active')
+
+ subscription.delete()
+
+ def test_create_subscription_trial_with_wrong_dates(self):
+ subscription = frappe.new_doc('Subscriptions')
+ subscription.subscriber = '_Test Customer'
+ subscription.trial_period_end = nowdate()
+ subscription.trial_period_start = add_days(nowdate(), 30)
+ subscription.append('plans', {'plan': '_Test Plan Name'})
+
+ self.assertRaises(frappe.ValidationError, subscription.save)
+ subscription.delete()
+
+ def test_create_subscription_multi_with_different_billing_fails(self):
+ subscription = frappe.new_doc('Subscriptions')
+ subscription.subscriber = '_Test Customer'
+ subscription.trial_period_end = nowdate()
+ subscription.trial_period_start = add_days(nowdate(), 30)
+ subscription.append('plans', {'plan': '_Test Plan Name'})
+ subscription.append('plans', {'plan': '_Test Plan Name 3'})
+
+ self.assertRaises(frappe.ValidationError, subscription.save)
+ subscription.delete()
+
+ def test_invoice_is_generated_at_end_of_billing_period(self):
+ subscription = frappe.new_doc('Subscriptions')
+ subscription.subscriber = '_Test Customer'
+ subscription.start = '2018-01-01'
+ subscription.append('plans', {'plan': '_Test Plan Name'})
+ subscription.insert()
+
+ self.assertEqual(subscription.status, 'Active')
+ self.assertEqual(subscription.current_invoice_start, '2018-01-01')
+ self.assertEqual(subscription.current_invoice_end, '2018-01-31')
+ subscription.process()
+
+ self.assertEqual(len(subscription.invoices), 1)
+ self.assertEqual(subscription.current_invoice_start, '2018-01-01')
+ self.assertEqual(subscription.status, 'Past Due Date')
+ subscription.delete()
+
+ def test_status_goes_back_to_active_after_invoice_is_paid(self):
+ subscription = frappe.new_doc('Subscriptions')
+ subscription.subscriber = '_Test Customer'
+ subscription.append('plans', {'plan': '_Test Plan Name'})
+ subscription.start = '2018-01-01'
+ subscription.insert()
+ subscription.process() # generate first invoice
+ self.assertEqual(len(subscription.invoices), 1)
+ self.assertEqual(subscription.status, 'Past Due Date')
+
+ subscription.get_current_invoice()
+ current_invoice = subscription.get_current_invoice()
+
+ self.assertIsNotNone(current_invoice)
+
+ current_invoice.db_set('outstanding_amount', 0)
+ current_invoice.db_set('status', 'Paid')
+ subscription.process()
+
+ self.assertEqual(subscription.status, 'Active')
+ self.assertEqual(subscription.current_invoice_start, nowdate())
+ self.assertEqual(len(subscription.invoices), 1)
+
+ subscription.delete()
+
+ def test_subscription_cancel_after_grace_period(self):
+ settings = frappe.get_single('Subscription Settings')
+ default_grace_period_action = settings.cancel_after_grace
+ settings.cancel_after_grace = 1
+ settings.save()
+
+ subscription = frappe.new_doc('Subscriptions')
+ subscription.subscriber = '_Test Customer'
+ subscription.append('plans', {'plan': '_Test Plan Name'})
+ subscription.start = '2018-01-01'
+ subscription.insert()
+ subscription.process() # generate first invoice
+
+ self.assertEqual(subscription.status, 'Past Due Date')
+
+ subscription.process()
+ # This should change status to Canceled since grace period is 0
+ self.assertEqual(subscription.status, 'Canceled')
+
+ settings.cancel_after_grace = default_grace_period_action
+ settings.save()
+ subscription.delete()
+
+ def test_subscription_unpaid_after_grace_period(self):
+ settings = frappe.get_single('Subscription Settings')
+ default_grace_period_action = settings.cancel_after_grace
+ settings.cancel_after_grace = 0
+ settings.save()
+
+ subscription = frappe.new_doc('Subscriptions')
+ subscription.subscriber = '_Test Customer'
+ subscription.append('plans', {'plan': '_Test Plan Name'})
+ subscription.start = '2018-01-01'
+ subscription.insert()
+ subscription.process() # generate first invoice
+
+ self.assertEqual(subscription.status, 'Past Due Date')
+
+ subscription.process()
+ # This should change status to Canceled since grace period is 0
+ self.assertEqual(subscription.status, 'Unpaid')
+
+ settings.cancel_after_grace = default_grace_period_action
+ settings.save()
+ subscription.delete()
+
+ def test_subscription_invoice_days_until_due(self):
+ subscription = frappe.new_doc('Subscriptions')
+ subscription.subscriber = '_Test Customer'
+ subscription.append('plans', {'plan': '_Test Plan Name'})
+ subscription.days_until_due = 10
+ subscription.start = add_months(nowdate(), -1)
+ subscription.insert()
+ subscription.process() # generate first invoice
+ self.assertEqual(len(subscription.invoices), 1)
+ self.assertEqual(subscription.status, 'Active')
+
+ subscription.delete()
+
+ def test_subscription_is_past_due_doesnt_change_within_grace_period(self):
+ settings = frappe.get_single('Subscription Settings')
+ grace_period = settings.grace_period
+ settings.grace_period = 1000
+ settings.save()
+
+ subscription = frappe.new_doc('Subscriptions')
+ subscription.subscriber = '_Test Customer'
+ subscription.append('plans', {'plan': '_Test Plan Name'})
+ subscription.start = '2018-01-01'
+ subscription.insert()
+ subscription.process() # generate first invoice
+
+ self.assertEqual(subscription.status, 'Past Due Date')
+
+ subscription.process()
+ # Grace period is 1000 days so status should remain as Past Due Date
+ self.assertEqual(subscription.status, 'Past Due Date')
+
+ subscription.process()
+ self.assertEqual(subscription.status, 'Past Due Date')
+
+ subscription.process()
+ self.assertEqual(subscription.status, 'Past Due Date')
+
+ settings.grace_period = grace_period
+ settings.save()
+ subscription.delete()
+
+ def test_subscription_remains_active_during_invoice_period(self):
+ subscription = frappe.new_doc('Subscriptions')
+ subscription.subscriber = '_Test Customer'
+ subscription.append('plans', {'plan': '_Test Plan Name'})
+ subscription.save()
+ subscription.process() # no changes expected
+
+ self.assertEqual(subscription.status, 'Active')
+ self.assertEqual(subscription.current_invoice_start, nowdate())
+ self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1))
+ self.assertEqual(len(subscription.invoices), 0)
+
+ subscription.process() # no changes expected still
+ self.assertEqual(subscription.status, 'Active')
+ self.assertEqual(subscription.current_invoice_start, nowdate())
+ self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1))
+ self.assertEqual(len(subscription.invoices), 0)
+
+ subscription.process() # no changes expected yet still
+ self.assertEqual(subscription.status, 'Active')
+ self.assertEqual(subscription.current_invoice_start, nowdate())
+ self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1))
+ self.assertEqual(len(subscription.invoices), 0)
+
+ subscription.delete()
+
+ def test_subscription_cancelation(self):
+ subscription = frappe.new_doc('Subscriptions')
+ subscription.subscriber = '_Test Customer'
+ subscription.append('plans', {'plan': '_Test Plan Name'})
+ subscription.save()
+ subscription.cancel_subscription()
+
+ self.assertEqual(subscription.status, 'Canceled')
+
+ subscription.delete()
+
+ def test_subscription_cancellation_invoices(self):
+ subscription = frappe.new_doc('Subscriptions')
+ subscription.subscriber = '_Test Customer'
+ subscription.append('plans', {'plan': '_Test Plan Name'})
+ subscription.save()
+
+ self.assertEqual(subscription.status, 'Active')
+
+ subscription.cancel_subscription()
+ # Invoice must have been generated
+ self.assertEqual(len(subscription.invoices), 1)
+
+ invoice = subscription.get_current_invoice()
+ diff = date_diff(nowdate(), subscription.current_invoice_start) + 1
+ plan_days = date_diff(subscription.current_invoice_end, subscription.current_invoice_start) + 1
+ prorate_factor = diff/plan_days
+
+ self.assertEqual(
+ get_prorata_factor(subscription.current_invoice_end, subscription.current_invoice_start),
+ prorate_factor
+ )
+ self.assertEqual(invoice.grand_total, prorate_factor * 900)
+ self.assertEqual(subscription.status, 'Canceled')
+
+ subscription.delete()
+
+ def test_subscription_cancellation_invoices_with_prorata_false(self):
+ settings = frappe.get_single('Subscription Settings')
+ to_prorate = settings.prorate
+ settings.prorate = 0
+ settings.save()
+
+ subscription = frappe.new_doc('Subscriptions')
+ subscription.subscriber = '_Test Customer'
+ subscription.append('plans', {'plan': '_Test Plan Name'})
+ subscription.save()
+ subscription.cancel_subscription()
+ invoice = subscription.get_current_invoice()
+
+ self.assertEqual(invoice.grand_total, 900)
+
+ settings.prorate = to_prorate
+ settings.save()
+
+ subscription.delete()
+
+ def test_subscription_cancellation_invoices_with_prorata_true(self):
+ settings = frappe.get_single('Subscription Settings')
+ to_prorate = settings.prorate
+ settings.prorate = 1
+ settings.save()
+
+ subscription = frappe.new_doc('Subscriptions')
+ subscription.subscriber = '_Test Customer'
+ subscription.append('plans', {'plan': '_Test Plan Name'})
+ subscription.save()
+ subscription.cancel_subscription()
+
+ invoice = subscription.get_current_invoice()
+ diff = date_diff(nowdate(), subscription.current_invoice_start) + 1
+ plan_days = date_diff(subscription.current_invoice_end, subscription.current_invoice_start) + 1
+ prorate_factor = diff/plan_days
+
+ self.assertEqual(invoice.grand_total, prorate_factor * 900)
+
+ settings.prorate = to_prorate
+ settings.save()
+
+ subscription.delete()
+
+ def test_subcription_cancellation_and_process(self):
+ settings = frappe.get_single('Subscription Settings')
+ default_grace_period_action = settings.cancel_after_grace
+ settings.cancel_after_grace = 1
+ settings.save()
+
+ subscription = frappe.new_doc('Subscriptions')
+ subscription.subscriber = '_Test Customer'
+ subscription.append('plans', {'plan': '_Test Plan Name'})
+ subscription.start = '2018-01-01'
+ subscription.insert()
+ subscription.process() # generate first invoice
+ invoices = len(subscription.invoices)
+
+ self.assertEqual(subscription.status, 'Past Due Date')
+ self.assertEqual(len(subscription.invoices), invoices)
+
+ subscription.cancel_subscription()
+ self.assertEqual(subscription.status, 'Canceled')
+ self.assertEqual(len(subscription.invoices), invoices)
+
+ subscription.process()
+ self.assertEqual(subscription.status, 'Canceled')
+ self.assertEqual(len(subscription.invoices), invoices)
+
+ subscription.process()
+ self.assertEqual(subscription.status, 'Canceled')
+ self.assertEqual(len(subscription.invoices), invoices)
+
+ settings.cancel_after_grace = default_grace_period_action
+ settings.save()
+ subscription.delete()
+
+ def test_subscription_restart_and_process(self):
+ settings = frappe.get_single('Subscription Settings')
+ default_grace_period_action = settings.cancel_after_grace
+ settings.grace_period = 0
+ settings.cancel_after_grace = 0
+ settings.save()
+
+ subscription = frappe.new_doc('Subscriptions')
+ subscription.subscriber = '_Test Customer'
+ subscription.append('plans', {'plan': '_Test Plan Name'})
+ subscription.start = '2018-01-01'
+ subscription.insert()
+ subscription.process() # generate first invoice
+
+ self.assertEqual(subscription.status, 'Past Due Date')
+
+ subscription.process()
+ self.assertEqual(subscription.status, 'Unpaid')
+
+ subscription.cancel_subscription()
+ self.assertEqual(subscription.status, 'Canceled')
+
+ subscription.restart_subscription()
+ self.assertEqual(subscription.status, 'Active')
+ self.assertEqual(len(subscription.invoices), 0)
+
+ subscription.process()
+ self.assertEqual(subscription.status, 'Active')
+ self.assertEqual(len(subscription.invoices), 0)
+
+ subscription.process()
+ self.assertEqual(subscription.status, 'Active')
+ self.assertEqual(len(subscription.invoices), 0)
+
+ settings.cancel_after_grace = default_grace_period_action
+ settings.save()
+ subscription.delete()
+
+ def test_subscription_unpaid_back_to_active(self):
+ settings = frappe.get_single('Subscription Settings')
+ default_grace_period_action = settings.cancel_after_grace
+ settings.cancel_after_grace = 0
+ settings.save()
+
+ subscription = frappe.new_doc('Subscriptions')
+ subscription.subscriber = '_Test Customer'
+ subscription.append('plans', {'plan': '_Test Plan Name'})
+ subscription.start = '2018-01-01'
+ subscription.insert()
+ subscription.process() # generate first invoice
+
+ self.assertEqual(subscription.status, 'Past Due Date')
+
+ subscription.process()
+ # This should change status to Canceled since grace period is 0
+ self.assertEqual(subscription.status, 'Unpaid')
+
+ invoice = subscription.get_current_invoice()
+ invoice.db_set('outstanding_amount', 0)
+ invoice.db_set('status', 'Paid')
+
+ subscription.process()
+ self.assertEqual(subscription.status, 'Active')
+
+ subscription.process()
+ self.assertEqual(subscription.status, 'Active')
+
+ settings.cancel_after_grace = default_grace_period_action
+ settings.save()
+ subscription.delete()
diff --git a/erpnext/docs/assets/img/articles/subscriber.png b/erpnext/docs/assets/img/articles/subscriber.png
new file mode 100644
index 0000000..e4ce64d
--- /dev/null
+++ b/erpnext/docs/assets/img/articles/subscriber.png
Binary files differ
diff --git a/erpnext/docs/assets/img/articles/subscription-1.png b/erpnext/docs/assets/img/articles/subscription-1.png
new file mode 100644
index 0000000..cbff30f9
--- /dev/null
+++ b/erpnext/docs/assets/img/articles/subscription-1.png
Binary files differ
diff --git a/erpnext/docs/assets/img/articles/subscription-plan.png b/erpnext/docs/assets/img/articles/subscription-plan.png
new file mode 100644
index 0000000..b60f796
--- /dev/null
+++ b/erpnext/docs/assets/img/articles/subscription-plan.png
Binary files differ
diff --git a/erpnext/docs/assets/img/articles/subscription-settings.png b/erpnext/docs/assets/img/articles/subscription-settings.png
new file mode 100644
index 0000000..405f0bf
--- /dev/null
+++ b/erpnext/docs/assets/img/articles/subscription-settings.png
Binary files differ
diff --git a/erpnext/docs/user/manual/en/accounts/articles/how-to-manage-subscriptions-with-erpnext.md b/erpnext/docs/user/manual/en/accounts/articles/how-to-manage-subscriptions-with-erpnext.md
new file mode 100644
index 0000000..97e6638
--- /dev/null
+++ b/erpnext/docs/user/manual/en/accounts/articles/how-to-manage-subscriptions-with-erpnext.md
@@ -0,0 +1,104 @@
+# How To Manage Subscriptions With ERPNext
+
+ERPNext now allows you to manage your subscriptions easily. A single subscription can contain multiple plans. At
+the same time, A single subscriber can also have multiple subscriptions. ERPNext also automatically manages your
+subscriptions for you by generating new invoices when due and changing the subscription status for you.
+
+## Related Doctypes
+### Subscriber
+Like its name suggests, the Subscriber Doctype represents your subscribers and each record is linked to a single
+Customer.
+
+<img alt="Subscriber form" class="screenshot" src="{{docs_base_url}}/assets/img/articles/subscriber.png">
+
+### Subscription Plan
+Each Subscription Plan is linked to a single Item and contains billing and pricing information on the Item. You can have
+multiple Subscription Plans for a single Item. An example of a situation where you would want this is where you have
+different prices for the same Item like when you have a basic option and premium option for a service.
+
+<img alt="Subscription Plan Form" class="screenshot" src="{{docs_base_url}}/assets/img/articles/subscription-plan.png">
+
+### Subscription Settings
+Subscription Settings is where you tweak the behaviour of the Subscription Doctype. For example, you can set a grace
+period for overdue invoices from it. You can also elect to have a subscription cancelled if an overdue invoice is not
+paid after the grace period.
+
+<img alt="Subscription Settings Form" class="screenshot" src="{{docs_base_url}}/assets/img/articles/subscription-settings.png">
+
+## Creating A Subscription
+To create a Subscription, go to the Subscription creation form
+`Explore > Accounts > Subscriptions`
+
+<img alt="Subscription form" class="screenshot" src="{{docs_base_url}}/assets/img/articles/subscription-1.png">
+
+Select a Subscriber.
+
+If you want to cancel a subscription at the end of the present billing cycle, check the 'Cancel At End Of Period'
+check box.
+
+Select the start date for the subscription. By default, the start date is today's date. (Optional).
+
+If you are giving the subscriber a trial, enter the Trial Period Start Date and Trial Period End Date.
+
+If your invoice is not payable immediately, you can set the number of days before the invoice will be due in the
+'Days Until Due' field.
+
+If you require more than one unit of a plan, set it in the 'Quantity' field. For instance, a web developer is subscribed
+to your web hosting service. The developer buys a plan for each customer. Instead of having multiple subscriptions for
+the same plan, you can simply increase the quantity as needed.
+
+In the 'Plan' table, add Subscription Plans as required. You may have multiple Subscription Plans in a single
+Subscription as long as they all have the same billing period cycle. If the same Subscriber needs to subscribe to
+plans with different billing cycles, you will have to use a separate subscription.
+
+Select a Sales Taxes and Charges Template if you need to charge tax in your invoices.
+
+Fill the relevant fields in the 'Discounts' section if you need to add discounts to your invoices.
+
+Click Save.
+
+### Subscription Status
+ERPNext Subscription has five status values:
+- **Trialling** - A subscription that is in trial period
+- **Active** - A subscription that does not have any unpaid invoice
+- **Past Due** - A subscription whose most recent invoice is unpaid but is still within the grace period
+- **Unpaid** - A subscription whose most recent invoice is unpaid and past the grace period
+- **Canceled** - A subscription whose most recent invoice is unpaid and past the grace period. In this state, ERPNext no longer monitors the subscription.
+
+### Subscription Processing In The Background
+Every one hour interval, ERPNext processes all Subscriptions and updates each for any change in status. It will
+create new invoices if need be. When an outstanding invoice is paid, ERPNext updates the subscription accordingly.
+
+### Manually Updating Subscriptions
+Once you have saved a subscription, you can change the 'Days Until Due', 'Quantity', 'Plans', 'Sales Taxes and Charges
+Template', 'Apply Additional Discount On', 'Additional Discount Percentage' and 'Additional Discount Amount' fields.
+
+Note that changing any of the values will reflect in newly generated invoices only. Previously generated invoices will
+not be changed.
+
+### Cancelling Subscriptions
+To cancel a Subscription, simply click the 'Cancel Subscription' button. The subscription will update its 'Cancellation
+Date' field and the subscription will no longer be monitored.
+
+If you are cancelling an active subscription, an invoice will immediately be generated. The generated invoice will be on
+pro-rata basis by default. If you want ERPNext always create an invoice for the full amount, uncheck the 'Prorate' field
+in Subsciption Settings.
+
+### Restarting Subscriptions
+To restart a canceled subscription, simply click the 'Restart Subscription' button. Note the Subscription will empty
+its invoices table. Note that the invoices will still exist but the Subscription will no longer track them. The start
+date of the subscription will also be changed to the date the Subscription is restarted. The start of the billing
+cycle will also be set to the date the Subscription is restarted.
+
+### Recalculating Subscriptions
+Some times, a Subscription's status might have changed but might not yet be reflected in the Subscription. You can force
+ERPNext to update the subscription by clicking 'Fetch Subscription Updates'.
+
+### Subscription Settings
+**Grace Period** represents the number of days after a subscriber's invoice becomes overdue that ERPNext should delay
+before changing the Subscription status to 'Canceled' or 'Unpaid'.
+
+**Cancel Invoice After Grace Period** would cause ERPNext to automatically cancel a subscription if it is not paid before the grace period elapses. This setting is off by default.
+
+**Prorate** would cause ERPNext to generate a prorated invoice when an active subscription is canceled by default.
+If you would prefer a full invoice, uncheck the setting.
\ No newline at end of file
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 3935f22..844763d 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -218,7 +218,8 @@
scheduler_events = {
"hourly": [
"erpnext.accounts.doctype.subscription.subscription.make_subscription_entry",
- 'erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group.trigger_emails'
+ 'erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group.trigger_emails',
+ "erpnext.assets.doctype.subscriptions.subscriptions.process_all"
],
"daily": [
"erpnext.stock.reorder_item.reorder_item",