From 007919c03ff21a1869c5a5bcadebd437a268ad6b Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Sat, 24 Feb 2018 11:19:06 +0100 Subject: [PATCH 01/73] New doctype - Subscriber: - Encapsulates a subscriber to a service or product - Contains subscription specific information --- .../accounts/doctype/subscriber/__init__.py | 0 .../accounts/doctype/subscriber/subscriber.js | 8 ++ .../doctype/subscriber/subscriber.json | 126 ++++++++++++++++++ .../accounts/doctype/subscriber/subscriber.py | 10 ++ .../doctype/subscriber/test_subscriber.js | 23 ++++ .../doctype/subscriber/test_subscriber.py | 10 ++ 6 files changed, 177 insertions(+) create mode 100644 erpnext/accounts/doctype/subscriber/__init__.py create mode 100644 erpnext/accounts/doctype/subscriber/subscriber.js create mode 100644 erpnext/accounts/doctype/subscriber/subscriber.json create mode 100644 erpnext/accounts/doctype/subscriber/subscriber.py create mode 100644 erpnext/accounts/doctype/subscriber/test_subscriber.js create mode 100644 erpnext/accounts/doctype/subscriber/test_subscriber.py diff --git a/erpnext/accounts/doctype/subscriber/__init__.py b/erpnext/accounts/doctype/subscriber/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/accounts/doctype/subscriber/subscriber.js b/erpnext/accounts/doctype/subscriber/subscriber.js new file mode 100644 index 0000000000..ba5cdf97f0 --- /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 0000000000..39a23b12fa --- /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": "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": "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-24 11:17:46.809140", + "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 0000000000..c0aabcfbc9 --- /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 0000000000..1fd4a1e011 --- /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 0000000000..e8684c3ff1 --- /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 From 2581e2e5d79e693987e735ecc6022560cd8a07f5 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Sat, 24 Feb 2018 11:31:42 +0100 Subject: [PATCH 02/73] New doctype - Subscription Plan: - Plan is to be linked to a Product - Plan will be used to create Invoice items - Subscriber can have multiple plans - Contains billing cycle information --- .../doctype/subscription_plan/__init__.py | 0 .../subscription_plan/subscription_plan.js | 8 + .../subscription_plan/subscription_plan.json | 254 ++++++++++++++++++ .../subscription_plan/subscription_plan.py | 10 + .../test_subscription_plan.js | 23 ++ .../test_subscription_plan.py | 10 + 6 files changed, 305 insertions(+) create mode 100644 erpnext/accounts/doctype/subscription_plan/__init__.py create mode 100644 erpnext/accounts/doctype/subscription_plan/subscription_plan.js create mode 100644 erpnext/accounts/doctype/subscription_plan/subscription_plan.json create mode 100644 erpnext/accounts/doctype/subscription_plan/subscription_plan.py create mode 100644 erpnext/accounts/doctype/subscription_plan/test_subscription_plan.js create mode 100644 erpnext/accounts/doctype/subscription_plan/test_subscription_plan.py diff --git a/erpnext/accounts/doctype/subscription_plan/__init__.py b/erpnext/accounts/doctype/subscription_plan/__init__.py new file mode 100644 index 0000000000..e69de29bb2 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 0000000000..9baacdd4ea --- /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 0000000000..6ad09c1c2b --- /dev/null +++ b/erpnext/accounts/doctype/subscription_plan/subscription_plan.json @@ -0,0 +1,254 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "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", + "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-24 11:31:23.066506", + "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 0000000000..a1ca4e4425 --- /dev/null +++ b/erpnext/accounts/doctype/subscription_plan/subscription_plan.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 SubscriptionPlan(Document): + pass 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 0000000000..3ceb9a6050 --- /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 0000000000..4a9b578acc --- /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 From a07811b67f54a0fa61795a58fb88288cdc1ffa1d Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Mon, 26 Feb 2018 04:23:34 +0100 Subject: [PATCH 03/73] adds new doctype - Subscriptions - encapsulates subscription - linked to a subscriber - subscription can contain many plans - contains history of generated invoices --- .../doctype/subscription_invoice/__init__.py | 0 .../subscription_invoice.js | 8 + .../subscription_invoice.json | 73 ++ .../subscription_invoice.py | 10 + .../test_subscription_invoice.js | 23 + .../test_subscription_invoice.py | 10 + .../subscription_plan_detail/__init__.py | 0 .../subscription_plan_detail.json | 73 ++ .../subscription_plan_detail.py | 10 + .../doctype/subscriptions/__init__.py | 0 .../doctype/subscriptions/subscriptions.js | 8 + .../doctype/subscriptions/subscriptions.json | 754 ++++++++++++++++++ .../doctype/subscriptions/subscriptions.py | 10 + .../subscriptions/test_subscriptions.js | 23 + .../subscriptions/test_subscriptions.py | 10 + 15 files changed, 1012 insertions(+) create mode 100644 erpnext/accounts/doctype/subscription_invoice/__init__.py create mode 100644 erpnext/accounts/doctype/subscription_invoice/subscription_invoice.js create mode 100644 erpnext/accounts/doctype/subscription_invoice/subscription_invoice.json create mode 100644 erpnext/accounts/doctype/subscription_invoice/subscription_invoice.py create mode 100644 erpnext/accounts/doctype/subscription_invoice/test_subscription_invoice.js create mode 100644 erpnext/accounts/doctype/subscription_invoice/test_subscription_invoice.py create mode 100644 erpnext/accounts/doctype/subscription_plan_detail/__init__.py create mode 100644 erpnext/accounts/doctype/subscription_plan_detail/subscription_plan_detail.json create mode 100644 erpnext/accounts/doctype/subscription_plan_detail/subscription_plan_detail.py create mode 100644 erpnext/accounts/doctype/subscriptions/__init__.py create mode 100644 erpnext/accounts/doctype/subscriptions/subscriptions.js create mode 100644 erpnext/accounts/doctype/subscriptions/subscriptions.json create mode 100644 erpnext/accounts/doctype/subscriptions/subscriptions.py create mode 100644 erpnext/accounts/doctype/subscriptions/test_subscriptions.js create mode 100644 erpnext/accounts/doctype/subscriptions/test_subscriptions.py diff --git a/erpnext/accounts/doctype/subscription_invoice/__init__.py b/erpnext/accounts/doctype/subscription_invoice/__init__.py new file mode 100644 index 0000000000..e69de29bb2 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 0000000000..40f9af303e --- /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 0000000000..e2ff51eede --- /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": 0, + "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 04:21:41.265055", + "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 0000000000..69ff3e54c1 --- /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 0000000000..15d3df2a63 --- /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 0000000000..1d542b0f7d --- /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_detail/__init__.py b/erpnext/accounts/doctype/subscription_plan_detail/__init__.py new file mode 100644 index 0000000000..e69de29bb2 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 0000000000..c112923397 --- /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 0000000000..04ec4afb13 --- /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/subscriptions/__init__.py b/erpnext/accounts/doctype/subscriptions/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.js b/erpnext/accounts/doctype/subscriptions/subscriptions.js new file mode 100644 index 0000000000..14eb9b60fe --- /dev/null +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.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('Subscriptions', { + refresh: function(frm) { + + } +}); diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.json b/erpnext/accounts/doctype/subscriptions/subscriptions.json new file mode 100644 index 0000000000..5bc428ad69 --- /dev/null +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.json @@ -0,0 +1,754 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "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": "subscription_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": "Subscription 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": "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": 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": "Start", + "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, + "description": "The date subscription ended if it has ended", + "fieldname": "end_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": "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, + "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": "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": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "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": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 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": 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, + "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": "Data", + "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, + "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-02-26 04:22:41.250547", + "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 0000000000..1c667f8526 --- /dev/null +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.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 Subscriptions(Document): + pass diff --git a/erpnext/accounts/doctype/subscriptions/test_subscriptions.js b/erpnext/accounts/doctype/subscriptions/test_subscriptions.js new file mode 100644 index 0000000000..b5fe4efd28 --- /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 0000000000..27f67e44f3 --- /dev/null +++ b/erpnext/accounts/doctype/subscriptions/test_subscriptions.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 TestSubscriptions(unittest.TestCase): + pass From 4d31255d5c9f1b6ca23ae5df4a007677f37280e3 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Mon, 26 Feb 2018 04:40:53 +0100 Subject: [PATCH 04/73] customer field in Subscriber should be Link --- erpnext/accounts/doctype/subscriber/subscriber.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/subscriber/subscriber.json b/erpnext/accounts/doctype/subscriber/subscriber.json index 39a23b12fa..28a57d8b1a 100644 --- a/erpnext/accounts/doctype/subscriber/subscriber.json +++ b/erpnext/accounts/doctype/subscriber/subscriber.json @@ -51,7 +51,7 @@ "collapsible": 0, "columns": 0, "fieldname": "customer", - "fieldtype": "Select", + "fieldtype": "Link", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -87,7 +87,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-02-24 11:17:46.809140", + "modified": "2018-02-26 04:40:16.510290", "modified_by": "Administrator", "module": "Accounts", "name": "Subscriber", From 4ca5e81aa345a9efafc3a9c08895f9d4e11f55b6 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Mon, 26 Feb 2018 04:47:19 +0100 Subject: [PATCH 05/73] adds description for `billing_interval_count` in subscription plan --- .../accounts/doctype/subscription_plan/subscription_plan.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/subscription_plan/subscription_plan.json b/erpnext/accounts/doctype/subscription_plan/subscription_plan.json index 6ad09c1c2b..07b9fb96c0 100644 --- a/erpnext/accounts/doctype/subscription_plan/subscription_plan.json +++ b/erpnext/accounts/doctype/subscription_plan/subscription_plan.json @@ -179,6 +179,7 @@ "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, @@ -215,7 +216,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-02-24 11:31:23.066506", + "modified": "2018-02-26 04:46:47.101040", "modified_by": "Administrator", "module": "Accounts", "name": "Subscription Plan", From 7c455ad4b61b4f1172099da8a1c9ef5ec24f00e0 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Mon, 26 Feb 2018 04:50:45 +0100 Subject: [PATCH 06/73] display invoices section field only if there are invoices --- erpnext/accounts/doctype/subscriptions/subscriptions.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.json b/erpnext/accounts/doctype/subscriptions/subscriptions.json index 5bc428ad69..70dc96d9ad 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.json +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.json @@ -647,6 +647,7 @@ "bold": 0, "collapsible": 0, "columns": 0, + "depends_on": "eval(doc.invoices)", "fieldname": "sb_3", "fieldtype": "Section Break", "hidden": 0, @@ -715,7 +716,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-02-26 04:22:41.250547", + "modified": "2018-02-26 04:49:46.141146", "modified_by": "Administrator", "module": "Accounts", "name": "Subscriptions", From dda6234caba37f431b57e6861884060fea17ff99 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Mon, 26 Feb 2018 04:52:13 +0100 Subject: [PATCH 07/73] remove subscription name and name by subscriber --- .../doctype/subscriptions/subscriptions.json | 34 ++----------------- 1 file changed, 2 insertions(+), 32 deletions(-) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.json b/erpnext/accounts/doctype/subscriptions/subscriptions.json index 70dc96d9ad..7108c30bc9 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.json +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.json @@ -3,6 +3,7 @@ "allow_guest_to_view": 0, "allow_import": 0, "allow_rename": 0, + "autoname": "field:subscriber", "beta": 0, "creation": "2018-02-26 04:13:14.153718", "custom": 0, @@ -12,37 +13,6 @@ "editable_grid": 1, "engine": "InnoDB", "fields": [ - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "subscription_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": "Subscription 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, @@ -716,7 +686,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-02-26 04:49:46.141146", + "modified": "2018-02-26 04:52:04.318912", "modified_by": "Administrator", "module": "Accounts", "name": "Subscriptions", From bbac07d9d28da09dda2078c9c97f2d3fe957e049 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Mon, 26 Feb 2018 05:07:56 +0100 Subject: [PATCH 08/73] use naming series for Subscriptions --- erpnext/accounts/doctype/subscriptions/subscriptions.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.json b/erpnext/accounts/doctype/subscriptions/subscriptions.json index 7108c30bc9..acadf6583d 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.json +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.json @@ -3,7 +3,7 @@ "allow_guest_to_view": 0, "allow_import": 0, "allow_rename": 0, - "autoname": "field:subscriber", + "autoname": "SUB.####", "beta": 0, "creation": "2018-02-26 04:13:14.153718", "custom": 0, @@ -686,7 +686,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-02-26 04:52:04.318912", + "modified": "2018-02-26 05:07:43.969220", "modified_by": "Administrator", "module": "Accounts", "name": "Subscriptions", From 988f5ca38bb440207de49bee9e971a3e22aef46e Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Mon, 26 Feb 2018 05:14:10 +0100 Subject: [PATCH 09/73] hide `trial_period_end` and `invoices` if empty --- erpnext/accounts/doctype/subscriptions/subscriptions.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.json b/erpnext/accounts/doctype/subscriptions/subscriptions.json index acadf6583d..61f83e155d 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.json +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.json @@ -270,6 +270,7 @@ "bold": 0, "collapsible": 0, "columns": 0, + "depends_on": "eval:doc.trial_period_start", "fieldname": "trial_period_end", "fieldtype": "Date", "hidden": 0, @@ -617,7 +618,7 @@ "bold": 0, "collapsible": 0, "columns": 0, - "depends_on": "eval(doc.invoices)", + "depends_on": "eval:doc.invoices", "fieldname": "sb_3", "fieldtype": "Section Break", "hidden": 0, @@ -686,7 +687,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-02-26 05:07:43.969220", + "modified": "2018-02-26 05:14:02.269620", "modified_by": "Administrator", "module": "Accounts", "name": "Subscriptions", From 9016890ecf8d7acab68de9680181c352e7b17efe Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Mon, 26 Feb 2018 06:15:31 +0100 Subject: [PATCH 10/73] adds Subscription Settings doctype --- .../doctype/subscription_settings/__init__.py | 0 .../subscription_settings.js | 8 ++ .../subscription_settings.json | 94 +++++++++++++++++++ .../subscription_settings.py | 10 ++ .../test_subscription_settings.js | 23 +++++ .../test_subscription_settings.py | 10 ++ 6 files changed, 145 insertions(+) create mode 100644 erpnext/accounts/doctype/subscription_settings/__init__.py create mode 100644 erpnext/accounts/doctype/subscription_settings/subscription_settings.js create mode 100644 erpnext/accounts/doctype/subscription_settings/subscription_settings.json create mode 100644 erpnext/accounts/doctype/subscription_settings/subscription_settings.py create mode 100644 erpnext/accounts/doctype/subscription_settings/test_subscription_settings.js create mode 100644 erpnext/accounts/doctype/subscription_settings/test_subscription_settings.py diff --git a/erpnext/accounts/doctype/subscription_settings/__init__.py b/erpnext/accounts/doctype/subscription_settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 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 0000000000..c4541c3719 --- /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 0000000000..2fa899943c --- /dev/null +++ b/erpnext/accounts/doctype/subscription_settings/subscription_settings.json @@ -0,0 +1,94 @@ +{ + "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": "0", + "fieldname": "cancel_invoice", + "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 If Unpaid", + "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 06:13:37.910139", + "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 + } + ], + "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 0000000000..3d382a7f5a --- /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 0000000000..5a751ea99c --- /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 0000000000..b9592d3cf4 --- /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 From 8457bb2c7e813ea6f55af98220208db3628d7070 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Mon, 26 Feb 2018 08:13:41 +0100 Subject: [PATCH 11/73] change in Subscription Settings --- .../subscription_settings.json | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/subscription_settings/subscription_settings.json b/erpnext/accounts/doctype/subscription_settings/subscription_settings.json index 2fa899943c..e692158402 100644 --- a/erpnext/accounts/doctype/subscription_settings/subscription_settings.json +++ b/erpnext/accounts/doctype/subscription_settings/subscription_settings.json @@ -12,6 +12,39 @@ "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, @@ -19,7 +52,7 @@ "collapsible": 0, "columns": 0, "default": "0", - "fieldname": "cancel_invoice", + "fieldname": "cancel_after_grace", "fieldtype": "Check", "hidden": 0, "ignore_user_permissions": 0, @@ -28,7 +61,7 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Cancel Invoice If Unpaid", + "label": "Cancel Invoice After Grace Period", "length": 0, "no_copy": 0, "permlevel": 0, @@ -55,7 +88,7 @@ "issingle": 1, "istable": 0, "max_attachments": 0, - "modified": "2018-02-26 06:13:37.910139", + "modified": "2018-02-26 08:12:59.168751", "modified_by": "Administrator", "module": "Accounts", "name": "Subscription Settings", From 066958a794382137975c86685bb1f05bc926c399 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Mon, 26 Feb 2018 11:22:05 +0100 Subject: [PATCH 12/73] subscription_invoice doctype changes --- .../doctype/subscription_invoice/subscription_invoice.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/subscription_invoice/subscription_invoice.json b/erpnext/accounts/doctype/subscription_invoice/subscription_invoice.json index e2ff51eede..c4bae1d3c3 100644 --- a/erpnext/accounts/doctype/subscription_invoice/subscription_invoice.json +++ b/erpnext/accounts/doctype/subscription_invoice/subscription_invoice.json @@ -25,7 +25,7 @@ "ignore_xss_filter": 0, "in_filter": 0, "in_global_search": 0, - "in_list_view": 0, + "in_list_view": 1, "in_standard_filter": 0, "label": "Invoice", "length": 0, @@ -55,7 +55,7 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2018-02-26 04:21:41.265055", + "modified": "2018-02-26 10:48:07.033422", "modified_by": "Administrator", "module": "Accounts", "name": "Subscription Invoice", From cbe4f5b6b37d70bcb1c1de05d176ff88f3d615e5 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Mon, 26 Feb 2018 11:22:53 +0100 Subject: [PATCH 13/73] changes to subscription settings --- .../subscription_settings.json | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/subscription_settings/subscription_settings.json b/erpnext/accounts/doctype/subscription_settings/subscription_settings.json index e692158402..b3327441dc 100644 --- a/erpnext/accounts/doctype/subscription_settings/subscription_settings.json +++ b/erpnext/accounts/doctype/subscription_settings/subscription_settings.json @@ -88,7 +88,7 @@ "issingle": 1, "istable": 0, "max_attachments": 0, - "modified": "2018-02-26 08:12:59.168751", + "modified": "2018-02-26 08:16:09.395304", "modified_by": "Administrator", "module": "Accounts", "name": "Subscription Settings", @@ -114,6 +114,26 @@ "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, From 2dee342501313e3e9411a0a6f042caef2ffc56d5 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Mon, 26 Feb 2018 13:34:25 +0100 Subject: [PATCH 14/73] Subscriptions doctype - trial start and end are set_only_once --- erpnext/accounts/doctype/subscriptions/subscriptions.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.json b/erpnext/accounts/doctype/subscriptions/subscriptions.json index 61f83e155d..85371780df 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.json +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.json @@ -260,7 +260,7 @@ "report_hide": 0, "reqd": 0, "search_index": 0, - "set_only_once": 0, + "set_only_once": 1, "translatable": 0, "unique": 0 }, @@ -292,7 +292,7 @@ "report_hide": 0, "reqd": 0, "search_index": 0, - "set_only_once": 0, + "set_only_once": 1, "translatable": 0, "unique": 0 }, @@ -319,7 +319,7 @@ "precision": "", "print_hide": 0, "print_hide_if_no_value": 0, - "read_only": 1, + "read_only": 0, "remember_last_selected_value": 0, "report_hide": 0, "reqd": 0, @@ -687,7 +687,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-02-26 05:14:02.269620", + "modified": "2018-02-26 13:34:04.108822", "modified_by": "Administrator", "module": "Accounts", "name": "Subscriptions", From 2b5298238e9e08f97d8c5d061c07dc34d2616eb7 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Mon, 26 Feb 2018 13:58:29 +0100 Subject: [PATCH 15/73] adds prorate field to Subscription Settings --- .../subscription_settings.json | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/subscription_settings/subscription_settings.json b/erpnext/accounts/doctype/subscription_settings/subscription_settings.json index b3327441dc..8c7c6f34e5 100644 --- a/erpnext/accounts/doctype/subscription_settings/subscription_settings.json +++ b/erpnext/accounts/doctype/subscription_settings/subscription_settings.json @@ -76,6 +76,38 @@ "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, @@ -88,7 +120,7 @@ "issingle": 1, "istable": 0, "max_attachments": 0, - "modified": "2018-02-26 08:16:09.395304", + "modified": "2018-02-26 13:58:09.455832", "modified_by": "Administrator", "module": "Accounts", "name": "Subscription Settings", From 6e014331c52fbfee4488a5a07e0a6c7e4d3cd6be Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Mon, 26 Feb 2018 14:05:54 +0100 Subject: [PATCH 16/73] removes end_date from Subscriptions doctype --- .../doctype/subscriptions/subscriptions.json | 34 +------------------ 1 file changed, 1 insertion(+), 33 deletions(-) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.json b/erpnext/accounts/doctype/subscriptions/subscriptions.json index 85371780df..33a7a251f3 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.json +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.json @@ -76,38 +76,6 @@ "translatable": 0, "unique": 0 }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "The date subscription ended if it has ended", - "fieldname": "end_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": "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, @@ -687,7 +655,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-02-26 13:34:04.108822", + "modified": "2018-02-26 14:05:40.359967", "modified_by": "Administrator", "module": "Accounts", "name": "Subscriptions", From 35e92c7d29dffad1443dfbd4234efd30cd1eebdf Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Mon, 26 Feb 2018 15:25:26 +0100 Subject: [PATCH 17/73] removes start field from Subscriptions --- .../doctype/subscriptions/subscriptions.json | 33 +------------------ 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.json b/erpnext/accounts/doctype/subscriptions/subscriptions.json index 33a7a251f3..5f25377639 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.json +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.json @@ -45,37 +45,6 @@ "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": "Start", - "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, @@ -655,7 +624,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-02-26 14:05:40.359967", + "modified": "2018-02-26 15:25:15.673438", "modified_by": "Administrator", "module": "Accounts", "name": "Subscriptions", From 3010996cc5216f084a3d65bd6a1bcd6e34b28d4f Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Mon, 26 Feb 2018 15:51:32 +0100 Subject: [PATCH 18/73] adds `before_insert` methods: - set current_invoice_start - set current_invoice_end --- .../doctype/subscriptions/subscriptions.py | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.py b/erpnext/accounts/doctype/subscriptions/subscriptions.py index 1c667f8526..353e83ddd5 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.py @@ -5,6 +5,25 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document +from frappe.utils.data import now, nowdate, getdate, cint, add_days, date_diff, get_last_day, get_first_day +from frappe import _ + + +SUBSCRIPTION_SETTINGS = frappe.get_single('Subscription Settings') + class Subscriptions(Document): - pass + def before_insert(self): + # update start just before the subscription doc is created + self.update_subscription_period() + + def update_subscription_period(self): + self.set_current_invoice_start() + self.set_current_invoice_end() + + def set_current_invoice_start(self, date=None): + if not date: + self.current_invoice_start = nowdate() + + def set_current_invoice_end(self): + self.current_invoice_end = get_last_day(self.current_invoice_start) From 7a2c6df645c9f747ae23d1e4699a1a0025c01889 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Mon, 26 Feb 2018 15:55:32 +0100 Subject: [PATCH 19/73] sets Subscription doc status in `before_save` --- .../doctype/subscriptions/subscriptions.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.py b/erpnext/accounts/doctype/subscriptions/subscriptions.py index 353e83ddd5..98f154860c 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.py @@ -27,3 +27,18 @@ class Subscriptions(Document): def set_current_invoice_end(self): self.current_invoice_end = get_last_day(self.current_invoice_start) + + def before_save(self): + self.set_status() + + def set_status(self): + if self.is_trialling(): + self.status = 'Trialling' + elif self.status == 'Past Due' and self.is_past_grace_period(): + self.status = 'Canceled' if cint(SUBSCRIPTION_SETTINGS.cancel_after_grace) else 'Unpaid' + elif self.current_invoice_is_past_due(): + self.status = 'Past Due' + elif self.is_new_subscription(): + self.status = 'Active' + # todo: then generate new invoice + From b72aac67294ae91899b4ff520cdef2512c6b579a Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Mon, 26 Feb 2018 15:56:47 +0100 Subject: [PATCH 20/73] adds validation: - ensure trial period dates are in order --- .../doctype/subscriptions/subscriptions.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.py b/erpnext/accounts/doctype/subscriptions/subscriptions.py index 98f154860c..435c364d99 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.py @@ -42,3 +42,33 @@ class Subscriptions(Document): self.status = 'Active' # todo: then generate new invoice + def is_past_grace_period(self): + current_invoice = self.get_current_invoice() + if self.current_invoice_is_past_due(current_invoice): + grace_period = cint(SUBSCRIPTION_SETTINGS.grace_period) + + return nowdate() > add_days(current_invoice.due_date, grace_period) + + def current_invoice_is_past_due(self, current_invoice=None): + if not current_invoice: + current_invoice = self.get_current_invoice() + + if not current_invoice: + return False + else: + return nowdate() > current_invoice.due_date + + def is_new_subscription(self): + return len(self.invoices) == 0 + + def validate(self): + self.validate_trial_period() + + def validate_trial_period(self): + 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')) + From d3fdcd9da586bc27613184fd33de40ef8575ea11 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Mon, 26 Feb 2018 16:03:56 +0100 Subject: [PATCH 21/73] set `after_insert` methods: - if subscription is not in trial period, generate sales invoice --- .../doctype/subscriptions/subscriptions.py | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.py b/erpnext/accounts/doctype/subscriptions/subscriptions.py index 435c364d99..1a07190e7d 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.py @@ -42,6 +42,17 @@ class Subscriptions(Document): self.status = 'Active' # todo: then generate new invoice + def is_trialling(self): + return not self.period_has_passed(self.trial_period_end) and self.is_new_subscription() + + def period_has_passed(self, end_date): + # todo: test for illegal time + if not end_date: + return True + + end_date = getdate(end_date) + return nowdate() > end_date + def is_past_grace_period(self): current_invoice = self.get_current_invoice() if self.current_invoice_is_past_due(current_invoice): @@ -58,6 +69,12 @@ class Subscriptions(Document): else: return nowdate() > current_invoice.due_date + def get_current_invoice(self): + if len(self.invoices): + current = self.invoices[-1] + doc = frappe.get_doc('Sales Invoice', current) + return doc + def is_new_subscription(self): return len(self.invoices) == 0 @@ -72,3 +89,67 @@ class Subscriptions(Document): 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): + if not self.is_trialling(): + self.generate_invoice() + + def generate_invoice(self): + invoice = self.create_invoice() + invoice.save() + invoice.submit() + self.append('invoices', {'invoice': invoice.name}) + self.subscription_updated(invoice) + + return invoice + + def create_invoice(self): + invoice = frappe.new_doc('Sales Invoice') + 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) + for item in items_list: + item['qty'] = self.quantity + invoice.append('items', item) + + # Taxes + # todo: tax template does not populate tax table + if self.tax_template: + invoice.taxes_and_charges = self.tax_template + + # Due date + if cint(self.days_until_due): + invoice.append( + 'payment_schedule', + { + 'due_date': add_days(nowdate(), cint(self.days_until_due)), + 'invoice_portion': 100 + } + ) + + # Discounts + if self.apply_additional_discount: + invoice.apply_discount_on = self.apply_additional_discount + + 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 + + return invoice + + def get_customer(self, subscriber_name): + return frappe.get_value('Subscriber', subscriber_name) + + def get_items_from_plans(self, plans): + plan_items = [plan.plan for plan in plans] + + if plan_items: + item_names = frappe.db.sql( + 'select item as item_code, cost as rate from `tabSubscription Plan` where name=%s', + plan_items, as_dict=1 + ) + return item_names + From 08a11b4d1f40dfe2a1078395dcd91cb83fa2867a Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Mon, 26 Feb 2018 16:13:45 +0100 Subject: [PATCH 22/73] sets default for `days_until_due` to 0 --- erpnext/accounts/doctype/subscriptions/subscriptions.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.json b/erpnext/accounts/doctype/subscriptions/subscriptions.json index 5f25377639..f572c92137 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.json +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.json @@ -239,6 +239,7 @@ "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", @@ -624,7 +625,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-02-26 15:25:15.673438", + "modified": "2018-02-26 16:13:09.759890", "modified_by": "Administrator", "module": "Accounts", "name": "Subscriptions", From 0091c86e80390548ebf9d06098c8dfcb2b33bc9d Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Mon, 26 Feb 2018 16:18:55 +0100 Subject: [PATCH 23/73] fix date comparison bug --- erpnext/accounts/doctype/subscriptions/subscriptions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.py b/erpnext/accounts/doctype/subscriptions/subscriptions.py index 1a07190e7d..cf82b129ce 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.py @@ -51,7 +51,7 @@ class Subscriptions(Document): return True end_date = getdate(end_date) - return nowdate() > end_date + return getdate(nowdate()) > getdate(end_date) def is_past_grace_period(self): current_invoice = self.get_current_invoice() @@ -83,7 +83,7 @@ class Subscriptions(Document): def validate_trial_period(self): if self.trial_period_start and self.trial_period_end: - if getdate(self.trial_period_end) > getdate(self.trial_period_start): + 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: From cd700efc0bcd37d1f0a687fdc1bf1f2041885f5c Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Mon, 26 Feb 2018 16:26:36 +0100 Subject: [PATCH 24/73] only generate invoice `after_insert` if subscription is not in trial period --- erpnext/accounts/doctype/subscriptions/subscriptions.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.py b/erpnext/accounts/doctype/subscriptions/subscriptions.py index cf82b129ce..d2bda3d04f 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.py @@ -24,6 +24,8 @@ class Subscriptions(Document): def set_current_invoice_start(self, date=None): if not date: self.current_invoice_start = nowdate() + elif self.trial_period_start and self.is_trialling(): + self.current_invoice_start = self.trial_period_start def set_current_invoice_end(self): self.current_invoice_end = get_last_day(self.current_invoice_start) @@ -153,3 +155,5 @@ class Subscriptions(Document): ) return item_names + def subscription_updated(self, invoice): + pass From 4d8175f9d9dab4606cfaec2c1fffee9c9933b800 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Mon, 26 Feb 2018 18:56:05 +0100 Subject: [PATCH 25/73] adds test cases and fix resulting bugs --- .../doctype/subscriptions/subscriptions.py | 5 +- .../subscriptions/test_subscriptions.py | 53 ++++++++++++++++++- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.py b/erpnext/accounts/doctype/subscriptions/subscriptions.py index d2bda3d04f..6b6fc5ff98 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.py @@ -28,7 +28,10 @@ class Subscriptions(Document): self.current_invoice_start = self.trial_period_start def set_current_invoice_end(self): - self.current_invoice_end = get_last_day(self.current_invoice_start) + if self.is_trialling(): + self.current_invoice_end = self.trial_period_end + else: + self.current_invoice_end = get_last_day(self.current_invoice_start) def before_save(self): self.set_status() diff --git a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py index 27f67e44f3..65c942339c 100644 --- a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py @@ -5,6 +5,57 @@ from __future__ import unicode_literals import frappe import unittest +from frappe.utils.data import nowdate, add_days, get_last_day + class TestSubscriptions(unittest.TestCase): - pass + 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 = 999.99 + plan.billing_interval = 'Month' + plan.billing_interval_count = 1 + 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, []) + + 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, get_last_day(nowdate())) + self.assertEqual(len(subscription.invoices), 1) + + subscription.delete() \ No newline at end of file From 1ea50559677ede66bf9b5a2119df9629683ac4b1 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Mon, 26 Feb 2018 20:17:59 +0100 Subject: [PATCH 26/73] fix another date comparison bug --- erpnext/accounts/doctype/subscriptions/subscriptions.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.py b/erpnext/accounts/doctype/subscriptions/subscriptions.py index 6b6fc5ff98..457b27706e 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.py @@ -72,12 +72,12 @@ class Subscriptions(Document): if not current_invoice: return False else: - return nowdate() > current_invoice.due_date + return getdate(nowdate()) > getdate(current_invoice.due_date) def get_current_invoice(self): if len(self.invoices): current = self.invoices[-1] - doc = frappe.get_doc('Sales Invoice', current) + doc = frappe.get_doc('Sales Invoice', current.invoice) return doc def is_new_subscription(self): @@ -103,6 +103,7 @@ class Subscriptions(Document): invoice.save() invoice.submit() self.append('invoices', {'invoice': invoice.name}) + self.save() # Validates all over again but we don't mind self.subscription_updated(invoice) return invoice From b84e9519c2ec1e1a5cc72e275f82e7f12d6b590e Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Mon, 26 Feb 2018 20:35:20 +0100 Subject: [PATCH 27/73] Subscriptions doctype - subscriber should be set only once --- erpnext/accounts/doctype/subscriptions/subscriptions.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.json b/erpnext/accounts/doctype/subscriptions/subscriptions.json index f572c92137..b2a5af6a3f 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.json +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.json @@ -41,7 +41,7 @@ "report_hide": 0, "reqd": 1, "search_index": 0, - "set_only_once": 0, + "set_only_once": 1, "translatable": 0, "unique": 0 }, @@ -625,7 +625,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-02-26 16:13:09.759890", + "modified": "2018-02-26 20:34:47.418645", "modified_by": "Administrator", "module": "Accounts", "name": "Subscriptions", From 048bcbf08053f2afe26cb20c7c35f466699af8c1 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Mon, 26 Feb 2018 22:17:06 +0100 Subject: [PATCH 28/73] more tests and code fixes --- .../doctype/subscriptions/subscriptions.py | 32 +++++++++++-------- .../subscriptions/test_subscriptions.py | 29 +++++++++++++++-- 2 files changed, 45 insertions(+), 16 deletions(-) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.py b/erpnext/accounts/doctype/subscriptions/subscriptions.py index 457b27706e..bee1a055bc 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.py @@ -22,10 +22,14 @@ class Subscriptions(Document): self.set_current_invoice_end() def set_current_invoice_start(self, date=None): - if not date: - self.current_invoice_start = nowdate() - elif self.trial_period_start and self.is_trialling(): + if self.trial_period_start and self.is_trialling(): self.current_invoice_start = self.trial_period_start + elif not date: + current_invoice = self.get_current_invoice() + if not current_invoice: + self.current_invoice_start = nowdate() + else: + self.current_invoice_start = current_invoice.posting_date def set_current_invoice_end(self): if self.is_trialling(): @@ -100,8 +104,6 @@ class Subscriptions(Document): def generate_invoice(self): invoice = self.create_invoice() - invoice.save() - invoice.submit() self.append('invoices', {'invoice': invoice.name}) self.save() # Validates all over again but we don't mind self.subscription_updated(invoice) @@ -125,14 +127,13 @@ class Subscriptions(Document): invoice.taxes_and_charges = self.tax_template # Due date - if cint(self.days_until_due): - invoice.append( - 'payment_schedule', - { - 'due_date': add_days(nowdate(), cint(self.days_until_due)), - 'invoice_portion': 100 - } - ) + invoice.append( + 'payment_schedule', + { + 'due_date': add_days(self.current_invoice_end, cint(self.days_until_due)), + 'invoice_portion': 100 + } + ) # Discounts if self.apply_additional_discount: @@ -144,6 +145,9 @@ class Subscriptions(Document): if self.additional_discount_amount: invoice.additional_discount_amount = self.additional_discount_amount + invoice.save() + invoice.submit() + return invoice def get_customer(self, subscriber_name): @@ -160,4 +164,4 @@ class Subscriptions(Document): return item_names def subscription_updated(self, invoice): - pass + self.update_subscription_period() diff --git a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py index 65c942339c..1715042d49 100644 --- a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe import unittest -from frappe.utils.data import nowdate, add_days, get_last_day +from frappe.utils.data import nowdate, add_days, get_last_day, cint, getdate class TestSubscriptions(unittest.TestCase): @@ -54,8 +54,33 @@ class TestSubscriptions(unittest.TestCase): 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_start, getdate(nowdate())) self.assertEqual(subscription.current_invoice_end, get_last_day(nowdate())) self.assertEqual(len(subscription.invoices), 1) + 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) + + 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.save() + + generated_invoice_name = subscription.invoices[-1].invoice + invoice = frappe.get_doc('Sales Invoice', generated_invoice_name) + + self.assertEqual( + invoice.due_date, + add_days(subscription.current_invoice_end, cint(subscription.days_until_due)) + ) + subscription.delete() \ No newline at end of file From 07313c281a806bc54723de88ccef75a81cfa4d8d Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Tue, 27 Feb 2018 09:00:13 +0100 Subject: [PATCH 29/73] adds more assertions to test cases --- erpnext/accounts/doctype/subscriptions/test_subscriptions.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py index 1715042d49..48e6461737 100644 --- a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py @@ -43,6 +43,7 @@ class TestSubscriptions(unittest.TestCase): 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() @@ -57,6 +58,7 @@ class TestSubscriptions(unittest.TestCase): self.assertEqual(subscription.current_invoice_start, getdate(nowdate())) self.assertEqual(subscription.current_invoice_end, get_last_day(nowdate())) self.assertEqual(len(subscription.invoices), 1) + self.assertEqual(subscription.status, 'Active') subscription.delete() @@ -83,4 +85,4 @@ class TestSubscriptions(unittest.TestCase): add_days(subscription.current_invoice_end, cint(subscription.days_until_due)) ) - subscription.delete() \ No newline at end of file + subscription.delete() From 559d01e17624f3326266bfddfda46b087c0505a5 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Tue, 27 Feb 2018 09:13:35 +0100 Subject: [PATCH 30/73] allows to rename subscription plan --- .../accounts/doctype/subscription_plan/subscription_plan.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/subscription_plan/subscription_plan.json b/erpnext/accounts/doctype/subscription_plan/subscription_plan.json index 07b9fb96c0..ab58e7c3c6 100644 --- a/erpnext/accounts/doctype/subscription_plan/subscription_plan.json +++ b/erpnext/accounts/doctype/subscription_plan/subscription_plan.json @@ -2,7 +2,7 @@ "allow_copy": 0, "allow_guest_to_view": 0, "allow_import": 0, - "allow_rename": 0, + "allow_rename": 1, "autoname": "field:plan_name", "beta": 0, "creation": "2018-02-24 11:31:23.066506", @@ -216,7 +216,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-02-26 04:46:47.101040", + "modified": "2018-02-27 09:12:58.330140", "modified_by": "Administrator", "module": "Accounts", "name": "Subscription Plan", From 3c26a7e86253d4ac8aa95d59a69ba9259415abd2 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Tue, 27 Feb 2018 09:36:29 +0100 Subject: [PATCH 31/73] adjusts query so multiple plans can be used to create single invoice --- erpnext/accounts/doctype/subscriptions/subscriptions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.py b/erpnext/accounts/doctype/subscriptions/subscriptions.py index bee1a055bc..6cd8934fb4 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.py @@ -158,8 +158,8 @@ class Subscriptions(Document): if plan_items: item_names = frappe.db.sql( - 'select item as item_code, cost as rate from `tabSubscription Plan` where name=%s', - plan_items, as_dict=1 + 'select item as item_code, cost as rate from `tabSubscription Plan` where name in %s', + (plan_items,), as_dict=1 ) return item_names From c634ca873762ff43694ff02030d6ff3139be53d0 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Tue, 27 Feb 2018 14:12:39 +0100 Subject: [PATCH 32/73] adds validation to billing_interval_count in Subscription Plan --- .../doctype/subscription_plan/subscription_plan.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/subscription_plan/subscription_plan.py b/erpnext/accounts/doctype/subscription_plan/subscription_plan.py index a1ca4e4425..4b8c8fcedd 100644 --- a/erpnext/accounts/doctype/subscription_plan/subscription_plan.py +++ b/erpnext/accounts/doctype/subscription_plan/subscription_plan.py @@ -7,4 +7,9 @@ import frappe from frappe.model.document import Document class SubscriptionPlan(Document): - pass + 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') From 45b6a1719f2445a9e16a6f22e76e2d9099b7ea0b Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Tue, 27 Feb 2018 15:14:19 +0100 Subject: [PATCH 33/73] refactor: - current_invoice _start and end should be determined by trial period or billing period - adds new functions to get billing period data --- .../doctype/subscriptions/subscriptions.py | 56 ++++++++++++++++--- 1 file changed, 49 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.py b/erpnext/accounts/doctype/subscriptions/subscriptions.py index 6cd8934fb4..0cccaebf1b 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document -from frappe.utils.data import now, nowdate, getdate, cint, add_days, date_diff, get_last_day, get_first_day +from frappe.utils.data import now, nowdate, getdate, cint, add_days, date_diff, get_last_day, get_first_day, add_to_date from frappe import _ @@ -25,17 +25,58 @@ class Subscriptions(Document): if self.trial_period_start and self.is_trialling(): self.current_invoice_start = self.trial_period_start elif not date: - current_invoice = self.get_current_invoice() - if not current_invoice: - self.current_invoice_start = nowdate() - else: - self.current_invoice_start = current_invoice.posting_date + self.current_invoice_start = nowdate() def set_current_invoice_end(self): if self.is_trialling(): self.current_invoice_end = self.trial_period_end else: - self.current_invoice_end = get_last_day(self.current_invoice_start) + 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): + return self.get_billing_cycle_data() + + def validate_plans_billing_cycle(self, billing_cycle_data): + 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): + 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): + 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 before_save(self): self.set_status() @@ -89,6 +130,7 @@ class Subscriptions(Document): def validate(self): self.validate_trial_period() + self.validate_plans_billing_cycle(self.get_billing_cycle_and_interval()) def validate_trial_period(self): if self.trial_period_start and self.trial_period_end: From 9bf029a16e836a97523ac475c88d3666aa5aaf7c Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Tue, 27 Feb 2018 15:19:02 +0100 Subject: [PATCH 34/73] generate new invoice only when `current_invoice_end` is past --- erpnext/accounts/doctype/subscriptions/subscriptions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.py b/erpnext/accounts/doctype/subscriptions/subscriptions.py index 0cccaebf1b..3866981a8a 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.py @@ -141,7 +141,8 @@ class Subscriptions(Document): frappe.throw(_('Both Trial Period Start Date and Trial Period End Date must be set')) def after_insert(self): - if not self.is_trialling(): + # todo: deal with users who collect prepayments. Maybe a new Subscription Invoice doctype? + if not self.is_trialling() and nowdate() > self.current_invoice_end: self.generate_invoice() def generate_invoice(self): From 95a4ca942932621ed7be47c023a9657114423380 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Tue, 27 Feb 2018 18:09:06 +0100 Subject: [PATCH 35/73] update test cases --- .../subscriptions/test_subscriptions.py | 58 ++++++++++++++----- 1 file changed, 45 insertions(+), 13 deletions(-) diff --git a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py index 48e6461737..abd2f3f64f 100644 --- a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe import unittest -from frappe.utils.data import nowdate, add_days, get_last_day, cint, getdate +from frappe.utils.data import nowdate, add_days, get_last_day, cint, getdate, add_to_date class TestSubscriptions(unittest.TestCase): @@ -19,6 +19,24 @@ class TestSubscriptions(unittest.TestCase): 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.99 + 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.99 + 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') @@ -55,9 +73,10 @@ class TestSubscriptions(unittest.TestCase): self.assertEqual(subscription.trial_period_start, None) self.assertEqual(subscription.trial_period_end, None) - self.assertEqual(subscription.current_invoice_start, getdate(nowdate())) - self.assertEqual(subscription.current_invoice_end, get_last_day(nowdate())) - self.assertEqual(len(subscription.invoices), 1) + 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() @@ -71,18 +90,31 @@ class TestSubscriptions(unittest.TestCase): self.assertRaises(frappe.ValidationError, subscription.save) - def test_subscription_invoice_days_until_due(self): + 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.save() + subscription.append('plans', {'plan': '_Test Plan Name 3'}) - generated_invoice_name = subscription.invoices[-1].invoice - invoice = frappe.get_doc('Sales Invoice', generated_invoice_name) + self.assertRaises(frappe.ValidationError, subscription.save) - self.assertEqual( - invoice.due_date, - add_days(subscription.current_invoice_end, cint(subscription.days_until_due)) - ) + # 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.save() - subscription.delete() + # generated_invoice_name = subscription.invoices[-1].invoice + # invoice = frappe.get_doc('Sales Invoice', generated_invoice_name) + + # self.assertEqual( + # invoice.due_date, + # add_days(subscription.current_invoice_end, cint(subscription.days_until_due)) + # ) + + # subscription.delete() + + def test_subscription_creation_with_multiple_plans(self): + pass From 3aaf693abdcb7e0822f7dc13f5679ce66a701a75 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Wed, 28 Feb 2018 05:12:18 +0100 Subject: [PATCH 36/73] adds task -`process` --- .../doctype/subscriptions/subscriptions.py | 47 +++++++++++++++++-- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.py b/erpnext/accounts/doctype/subscriptions/subscriptions.py index 3866981a8a..e5c2cc55df 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.py @@ -13,6 +13,9 @@ SUBSCRIPTION_SETTINGS = frappe.get_single('Subscription Settings') class Subscriptions(Document): + def before_save(self): + self.set_status() + def before_insert(self): # update start just before the subscription doc is created self.update_subscription_period() @@ -78,14 +81,13 @@ class Subscriptions(Document): return data - def before_save(self): - self.set_status() - def set_status(self): if self.is_trialling(): self.status = 'Trialling' elif self.status == 'Past Due' and self.is_past_grace_period(): self.status = 'Canceled' if cint(SUBSCRIPTION_SETTINGS.cancel_after_grace) else 'Unpaid' + elif self.status == 'Past Due' and not self.has_outstanding_invoice(): + self.status = 'Active' elif self.current_invoice_is_past_due(): self.status = 'Past Due' elif self.is_new_subscription(): @@ -142,8 +144,7 @@ class Subscriptions(Document): def after_insert(self): # todo: deal with users who collect prepayments. Maybe a new Subscription Invoice doctype? - if not self.is_trialling() and nowdate() > self.current_invoice_end: - self.generate_invoice() + pass def generate_invoice(self): invoice = self.create_invoice() @@ -208,3 +209,39 @@ class Subscriptions(Document): def subscription_updated(self, invoice): self.update_subscription_period() + + def process(self): + """ + To be called by task periodically. It checks the subscription and takes appropriate action + as need be. It calls these methods in this order: + 1. `process_for_active` + 2. `process_for_past_due` + 3. + """ + if self.status == 'Active': + self.process_for_active() + elif self.status == 'Past Due': + self.process_for_past_due_date() + # process_for_unpaid() + + def process_for_active(self): + print 'processing for active' + if nowdate() > self.current_invoice_end and not self.has_outstanding_invoice(): + print 'generating invoice' + self.generate_invoice() + print 'invoice generated' + + if self.current_invoice_is_past_due(): + print 'current invoice is past due' + self.status = 'Past Due' + + def process_for_past_due_date(self): + if not self.has_outstanding_invoice(): + self.status = 'Active' + self.update_subscription_period() + else: + self.set_status() + + def has_outstanding_invoice(self): + current_invoice = self.get_current_invoice() + return current_invoice.posting_date != self.current_invoice_start From b3d5777e55566cdc4009ec4c2619459fef0fb5a8 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Wed, 28 Feb 2018 12:11:19 +0100 Subject: [PATCH 37/73] more test cases and bug fixes --- .../doctype/subscriptions/subscriptions.py | 60 ++++++---- .../subscriptions/test_subscriptions.py | 104 +++++++++++++++--- 2 files changed, 126 insertions(+), 38 deletions(-) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.py b/erpnext/accounts/doctype/subscriptions/subscriptions.py index e5c2cc55df..9a130f3360 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.py @@ -13,9 +13,6 @@ SUBSCRIPTION_SETTINGS = frappe.get_single('Subscription Settings') class Subscriptions(Document): - def before_save(self): - self.set_status() - def before_insert(self): # update start just before the subscription doc is created self.update_subscription_period() @@ -29,6 +26,8 @@ class Subscriptions(Document): 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): if self.is_trialling(): @@ -81,15 +80,19 @@ class Subscriptions(Document): return data - def set_status(self): + def set_status_grace_period(self): + 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): if self.is_trialling(): self.status = 'Trialling' - elif self.status == 'Past Due' and self.is_past_grace_period(): + elif self.status == 'Past Due Date' and self.is_past_grace_period(): self.status = 'Canceled' if cint(SUBSCRIPTION_SETTINGS.cancel_after_grace) else 'Unpaid' - elif self.status == 'Past Due' and not self.has_outstanding_invoice(): + 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' + self.status = 'Past Due Date' elif self.is_new_subscription(): self.status = 'Active' # todo: then generate new invoice @@ -110,7 +113,7 @@ class Subscriptions(Document): if self.current_invoice_is_past_due(current_invoice): grace_period = cint(SUBSCRIPTION_SETTINGS.grace_period) - return nowdate() > add_days(current_invoice.due_date, grace_period) + return getdate(nowdate()) > add_days(current_invoice.due_date, grace_period) def current_invoice_is_past_due(self, current_invoice=None): if not current_invoice: @@ -124,8 +127,11 @@ class Subscriptions(Document): def get_current_invoice(self): if len(self.invoices): current = self.invoices[-1] - doc = frappe.get_doc('Sales Invoice', current.invoice) - return doc + 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(invoice.invoice))) def is_new_subscription(self): return len(self.invoices) == 0 @@ -144,7 +150,7 @@ class Subscriptions(Document): def after_insert(self): # todo: deal with users who collect prepayments. Maybe a new Subscription Invoice doctype? - pass + self.set_subscription_status() def generate_invoice(self): invoice = self.create_invoice() @@ -156,6 +162,8 @@ class Subscriptions(Document): def create_invoice(self): 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` @@ -220,28 +228,36 @@ class Subscriptions(Document): """ if self.status == 'Active': self.process_for_active() - elif self.status == 'Past Due': + elif self.status == 'Past Due Date': self.process_for_past_due_date() + self.save() # process_for_unpaid() def process_for_active(self): - print 'processing for active' if nowdate() > self.current_invoice_end and not self.has_outstanding_invoice(): - print 'generating invoice' self.generate_invoice() - print 'invoice generated' if self.current_invoice_is_past_due(): - print 'current invoice is past due' - self.status = 'Past Due' + self.status = 'Past Due Date' def process_for_past_due_date(self): - if not self.has_outstanding_invoice(): - self.status = 'Active' - self.update_subscription_period() + current_invoice = self.get_current_invoice() + if not current_invoice: + frappe.throw('Current invoice is missing') else: - self.set_status() + if self.is_not_outstanding(current_invoice): + self.status = 'Active' + self.update_subscription_period() + else: + self.set_status_grace_period() + + def is_not_outstanding(self, invoice): + return invoice.status == 'Paid' def has_outstanding_invoice(self): current_invoice = self.get_current_invoice() - return current_invoice.posting_date != self.current_invoice_start + if not current_invoice: + return False + else: + return not self.is_not_outstanding(current_invoice) + return True diff --git a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py index abd2f3f64f..63177e4eae 100644 --- a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py @@ -5,7 +5,8 @@ from __future__ import unicode_literals import frappe import unittest -from frappe.utils.data import nowdate, add_days, get_last_day, cint, getdate, add_to_date +from frappe.utils.data import nowdate, add_days, get_last_day, cint, getdate, add_to_date, get_datetime_str +from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry class TestSubscriptions(unittest.TestCase): @@ -14,7 +15,7 @@ class TestSubscriptions(unittest.TestCase): plan = frappe.new_doc('Subscription Plan') plan.plan_name = '_Test Plan Name' plan.item = '_Test Non Stock Item' - plan.cost = 999.99 + plan.cost = 900 plan.billing_interval = 'Month' plan.billing_interval_count = 1 plan.insert() @@ -23,7 +24,7 @@ class TestSubscriptions(unittest.TestCase): plan = frappe.new_doc('Subscription Plan') plan.plan_name = '_Test Plan Name 2' plan.item = '_Test Non Stock Item' - plan.cost = 1999.99 + plan.cost = 1999 plan.billing_interval = 'Month' plan.billing_interval_count = 1 plan.insert() @@ -32,7 +33,7 @@ class TestSubscriptions(unittest.TestCase): plan = frappe.new_doc('Subscription Plan') plan.plan_name = '_Test Plan Name 3' plan.item = '_Test Non Stock Item' - plan.cost = 1999.99 + plan.cost = 1999 plan.billing_interval = 'Day' plan.billing_interval_count = 14 plan.insert() @@ -89,6 +90,7 @@ class TestSubscriptions(unittest.TestCase): 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') @@ -99,22 +101,92 @@ class TestSubscriptions(unittest.TestCase): subscription.append('plans', {'plan': '_Test Plan Name 3'}) self.assertRaises(frappe.ValidationError, subscription.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.save() + def test_invoice_is_generated_at_end_of_billing_period(self): + subscription = frappe.new_doc('Subscriptions') + subscription.subscriber = '_Test Customer' + subscription.append('plans', {'plan': '_Test Plan Name'}) + subscription.insert() - # generated_invoice_name = subscription.invoices[-1].invoice - # invoice = frappe.get_doc('Sales Invoice', generated_invoice_name) + self.assertEqual(subscription.status, 'Active') - # self.assertEqual( - # invoice.due_date, - # add_days(subscription.current_invoice_end, cint(subscription.days_until_due)) - # ) + subscription.set_current_invoice_start('2018-01-01') + subscription.set_current_invoice_end() - # subscription.delete() + 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, nowdate()) + 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.insert() + subscription.set_current_invoice_start('2018-01-01') + subscription.set_current_invoice_end() + 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.insert() + subscription.set_current_invoice_start('2018-01-01') + subscription.set_current_invoice_end() + subscription.process() # generate first invoice + + self.assertEqual(subscription.status, 'Past Due Date') + + subscription.process() + # This should change status to Canceled since grace period is 0 + self.assertEqual(subscription.status, 'Canceled') + + 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.insert() + subscription.set_current_invoice_start(get_datetime_str(add_to_date(nowdate(), months=-1))) + subscription.set_current_invoice_end() + subscription.process() # generate first invoice + self.assertEqual(len(subscription.invoices), 1) + self.assertEqual(subscription.status, 'Active') + + subscription.delete() def test_subscription_creation_with_multiple_plans(self): pass From 86ee455cbca3e689614ca59e34463f71478eefa4 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Wed, 28 Feb 2018 12:21:08 +0100 Subject: [PATCH 38/73] more tests and bug fix --- .../doctype/subscriptions/subscriptions.py | 12 +++++----- .../subscriptions/test_subscriptions.py | 23 +++++++++++++++++++ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.py b/erpnext/accounts/doctype/subscriptions/subscriptions.py index 9a130f3360..eed4e87508 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.py @@ -9,9 +9,6 @@ from frappe.utils.data import now, nowdate, getdate, cint, add_days, date_diff, from frappe import _ -SUBSCRIPTION_SETTINGS = frappe.get_single('Subscription Settings') - - class Subscriptions(Document): def before_insert(self): # update start just before the subscription doc is created @@ -81,14 +78,16 @@ class Subscriptions(Document): return data def set_status_grace_period(self): + 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' + self.status = 'Canceled' if cint(subscription_settings.cancel_after_grace) else 'Unpaid' def set_subscription_status(self): if self.is_trialling(): self.status = 'Trialling' elif self.status == 'Past Due Date' and self.is_past_grace_period(): - self.status = 'Canceled' if cint(SUBSCRIPTION_SETTINGS.cancel_after_grace) else 'Unpaid' + 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(): @@ -111,7 +110,8 @@ class Subscriptions(Document): def is_past_grace_period(self): current_invoice = self.get_current_invoice() if self.current_invoice_is_past_due(current_invoice): - grace_period = cint(SUBSCRIPTION_SETTINGS.grace_period) + 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) diff --git a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py index 63177e4eae..01d05d2934 100644 --- a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py @@ -173,6 +173,29 @@ class TestSubscriptions(unittest.TestCase): 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.insert() + subscription.set_current_invoice_start('2018-01-01') + subscription.set_current_invoice_end() + subscription.process() # generate first invoice + + self.assertEqual(subscription.status, 'Past Due Date') + + subscription.process() + # This should change status to Canceled since grace period is 0 + self.assertEqual(subscription.status, 'Unpaid') + + 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') From a5bc057ad44fdac3fd27710bd8851a950655ed9b Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Wed, 28 Feb 2018 12:24:16 +0100 Subject: [PATCH 39/73] adds missing 'translatability' --- erpnext/accounts/doctype/subscriptions/subscriptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.py b/erpnext/accounts/doctype/subscriptions/subscriptions.py index eed4e87508..e47b7ead4f 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.py @@ -243,7 +243,7 @@ class Subscriptions(Document): def process_for_past_due_date(self): current_invoice = self.get_current_invoice() if not current_invoice: - frappe.throw('Current invoice is missing') + frappe.throw(_('Current invoice {0} is missing'.format(current_invoice.invoice))) else: if self.is_not_outstanding(current_invoice): self.status = 'Active' From 2cccad0274eec7a737d023004386981dd826041f Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Wed, 28 Feb 2018 12:45:09 +0100 Subject: [PATCH 40/73] more tests and quick fix --- .../doctype/subscriptions/subscriptions.py | 2 +- .../subscriptions/test_subscriptions.py | 56 +++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.py b/erpnext/accounts/doctype/subscriptions/subscriptions.py index e47b7ead4f..123cd328c5 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.py @@ -234,7 +234,7 @@ class Subscriptions(Document): # process_for_unpaid() def process_for_active(self): - if nowdate() > self.current_invoice_end and not self.has_outstanding_invoice(): + if getdate(nowdate()) > getdate(self.current_invoice_end) and not self.has_outstanding_invoice(): self.generate_invoice() if self.current_invoice_is_past_due(): diff --git a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py index 01d05d2934..a3413c7b1e 100644 --- a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py @@ -211,5 +211,61 @@ class TestSubscriptions(unittest.TestCase): subscription.delete() + def test_subcription_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.insert() + subscription.set_current_invoice_start('2018-01-01') + subscription.set_current_invoice_end() + subscription.process() # generate first invoice + + self.assertEqual(subscription.status, 'Past Due Date') + + subscription.process() + # 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_creation_with_multiple_plans(self): pass From 8f37926ca89cf477fab2adaa6a2ec708a9e35e89 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Thu, 1 Mar 2018 04:50:04 +0100 Subject: [PATCH 41/73] add ability to cancel, restart and refresh subscription --- .../doctype/subscriptions/subscriptions.js | 69 ++++++++++ .../doctype/subscriptions/subscriptions.py | 55 ++++++-- .../subscriptions/test_subscriptions.py | 118 ++++++++++++++++++ 3 files changed, 235 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.js b/erpnext/accounts/doctype/subscriptions/subscriptions.js index 14eb9b60fe..ae572d24b7 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.js +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.js @@ -3,6 +3,75 @@ frappe.ui.form.on('Subscriptions', { refresh: function(frm) { + if(!frm.is_new()){ + if(frm.doc.status !== 'Canceled'){ + frm.add_custom_button( + __('Cancel Subscription'), + () => frm.events.cancel_this_subscription(frm) + ); + frm.add_custom_button( + __('Fetch Subscription Updates'), + () => frm.events.get_subscription_updates(frm) + ); + } + else if(frm.doc.status === 'Canceled'){ + frm.add_custom_button( + __('Restart Subscription'), + () => frm.events.renew_this_subscription(frm) + ); + } + } + }, + cancel_this_subscription: function(frm) { + const doc = frm.doc; + frappe.confirm( + __('This action will stop future billing. Are you sure you want to cancel this subscription?'), + function() { + frappe.call({ + method: + "erpnext.accounts.doctype.subscriptions.subscriptions.cancel_subscription", + args: {name: doc.name}, + callback: function(data){ + if(!data.exc){ + frm.reload_doc(); + } + } + }); + } + ); + }, + + renew_this_subscription: function(frm) { + const doc = frm.doc; + frappe.confirm( + __('You will lose records of previously generated invoices. Are you sure you want to restart this subscription?'), + function() { + frappe.call({ + method: + "erpnext.accounts.doctype.subscriptions.subscriptions.restart_subscription", + args: {name: doc.name}, + callback: function(data){ + if(!data.exc){ + frm.reload_doc(); + } + } + }); + } + ); + }, + + get_subscription_updates: function(frm) { + const doc = frm.doc; + frappe.call({ + method: + "erpnext.accounts.doctype.subscriptions.subscriptions.get_subscription_updates", + args: {name: doc.name}, + callback: function(data){ + if(!data.exc){ + frm.reload_doc(); + } + } + }); } }); diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.py b/erpnext/accounts/doctype/subscriptions/subscriptions.py index 123cd328c5..cd43a9cca4 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.py @@ -14,8 +14,8 @@ class Subscriptions(Document): # update start just before the subscription doc is created self.update_subscription_period() - def update_subscription_period(self): - self.set_current_invoice_start() + def update_subscription_period(self, date=None): + self.set_current_invoice_start(date) self.set_current_invoice_end() def set_current_invoice_start(self, date=None): @@ -228,16 +228,19 @@ class Subscriptions(Document): """ if self.status == 'Active': self.process_for_active() - elif self.status == 'Past Due Date': + elif self.status in ['Past Due Date', 'Unpaid']: self.process_for_past_due_date() - self.save() - # process_for_unpaid() + + if self.status != 'Canceled': + self.save() def process_for_active(self): if getdate(nowdate()) > getdate(self.current_invoice_end) and not self.has_outstanding_invoice(): self.generate_invoice() + if self.current_invoice_is_past_due(): + self.status = 'Past Due Date' - if self.current_invoice_is_past_due(): + if self.current_invoice_is_past_due() and getdate(nowdate()) > getdate(self.current_invoice_end): self.status = 'Past Due Date' def process_for_past_due_date(self): @@ -247,7 +250,7 @@ class Subscriptions(Document): else: if self.is_not_outstanding(current_invoice): self.status = 'Active' - self.update_subscription_period() + self.update_subscription_period(nowdate()) else: self.set_status_grace_period() @@ -261,3 +264,41 @@ class Subscriptions(Document): else: return not self.is_not_outstanding(current_invoice) return True + + def cancel_subscription(self): + """ + This sets the subscription as cancelled. It will stop invoices from being generated + but it will not affect already created invoices. + """ + self.status = 'Canceled' + self.cancelation_date = nowdate() + self.save() + + def restart_subscription(self): + """ + This sets the subscription as active. The subscription will be made to be like a new + subscription but new trial periods will not be allowed. + """ + self.status = 'Active' + self.cancelation_date = None + self.update_subscription_period(nowdate()) + self.invoices = [] + self.save() + + +@frappe.whitelist() +def cancel_subscription(name): + subscription = frappe.get_doc('Subscriptions', name) + subscription.cancel_subscription() + + +@frappe.whitelist() +def restart_subscription(name): + subscription = frappe.get_doc('Subscriptions', name) + subscription.restart_subscription() + + +@frappe.whitelist() +def get_subscription_updates(name): + subscription = frappe.get_doc('Subscriptions', name) + subscription.process() \ No newline at end of file diff --git a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py index a3413c7b1e..a0f940098b 100644 --- a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py @@ -267,5 +267,123 @@ class TestSubscriptions(unittest.TestCase): subscription.delete() + def test_subcription_cancelation(self): + subscription = frappe.new_doc('Subscriptions') + subscription.subscriber = '_Test Customer' + subscription.append('plans', {'plan': '_Test Plan Name'}) + subscription.save() + subscription.cancel_subscription() + + self.assertEqual(subscription.status, 'Canceled') + + subscription.delete() + + def test_subcription_cancelation_and_process(self): + settings = frappe.get_single('Subscription Settings') + default_grace_period_action = settings.cancel_after_grace + settings.cancel_after_grace = 1 + settings.save() + + subscription = frappe.new_doc('Subscriptions') + subscription.subscriber = '_Test Customer' + subscription.append('plans', {'plan': '_Test Plan Name'}) + subscription.insert() + subscription.set_current_invoice_start('2018-01-01') + subscription.set_current_invoice_end() + subscription.process() # generate first invoice + invoices = len(subscription.invoices) + + self.assertEqual(subscription.status, 'Past Due Date') + self.assertEqual(len(subscription.invoices), invoices) + + subscription.cancel_subscription() + self.assertEqual(subscription.status, 'Canceled') + self.assertEqual(len(subscription.invoices), invoices) + + subscription.process() + self.assertEqual(subscription.status, 'Canceled') + self.assertEqual(len(subscription.invoices), invoices) + + subscription.process() + self.assertEqual(subscription.status, 'Canceled') + self.assertEqual(len(subscription.invoices), invoices) + + settings.cancel_after_grace = default_grace_period_action + settings.save() + subscription.delete() + + def test_subscription_restart_and_process(self): + settings = frappe.get_single('Subscription Settings') + default_grace_period_action = settings.cancel_after_grace + settings.grace_period = 0 + settings.cancel_after_grace = 0 + settings.save() + + subscription = frappe.new_doc('Subscriptions') + subscription.subscriber = '_Test Customer' + subscription.append('plans', {'plan': '_Test Plan Name'}) + subscription.insert() + subscription.set_current_invoice_start('2018-01-01') + subscription.set_current_invoice_end() + subscription.process() # generate first invoice + + self.assertEqual(subscription.status, 'Past Due Date') + + subscription.process() + self.assertEqual(subscription.status, 'Unpaid') + + subscription.cancel_subscription() + self.assertEqual(subscription.status, 'Canceled') + + subscription.restart_subscription() + self.assertEqual(subscription.status, 'Active') + self.assertEqual(len(subscription.invoices), 0) + + subscription.process() + self.assertEqual(subscription.status, 'Active') + self.assertEqual(len(subscription.invoices), 0) + + subscription.process() + self.assertEqual(subscription.status, 'Active') + self.assertEqual(len(subscription.invoices), 0) + + settings.cancel_after_grace = default_grace_period_action + settings.save() + subscription.delete() + + def test_subscription_unpaid_back_to_active(self): + settings = frappe.get_single('Subscription Settings') + default_grace_period_action = settings.cancel_after_grace + settings.cancel_after_grace = 0 + settings.save() + + subscription = frappe.new_doc('Subscriptions') + subscription.subscriber = '_Test Customer' + subscription.append('plans', {'plan': '_Test Plan Name'}) + subscription.insert() + subscription.set_current_invoice_start('2018-01-01') + subscription.set_current_invoice_end() + subscription.process() # generate first invoice + + self.assertEqual(subscription.status, 'Past Due Date') + + subscription.process() + # This should change status to Canceled since grace period is 0 + self.assertEqual(subscription.status, 'Unpaid') + + invoice = subscription.get_current_invoice() + invoice.db_set('outstanding_amount', 0) + invoice.db_set('status', 'Paid') + + subscription.process() + self.assertEqual(subscription.status, 'Active') + + subscription.process() + self.assertEqual(subscription.status, 'Active') + + settings.cancel_after_grace = default_grace_period_action + settings.save() + subscription.delete() + def test_subscription_creation_with_multiple_plans(self): pass From d9fec69a709eaa34e28750bc214337e820be51e6 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Thu, 1 Mar 2018 05:34:02 +0100 Subject: [PATCH 42/73] update test case, fix bugs --- erpnext/accounts/doctype/subscriptions/subscriptions.py | 8 ++------ .../accounts/doctype/subscriptions/test_subscriptions.py | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.py b/erpnext/accounts/doctype/subscriptions/subscriptions.py index cd43a9cca4..a56a2f7467 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.py @@ -155,8 +155,7 @@ class Subscriptions(Document): def generate_invoice(self): invoice = self.create_invoice() self.append('invoices', {'invoice': invoice.name}) - self.save() # Validates all over again but we don't mind - self.subscription_updated(invoice) + self.save() return invoice @@ -174,9 +173,9 @@ class Subscriptions(Document): invoice.append('items', item) # Taxes - # todo: tax template does not populate tax table if self.tax_template: invoice.taxes_and_charges = self.tax_template + invoice.set_taxes() # Due date invoice.append( @@ -215,9 +214,6 @@ class Subscriptions(Document): ) return item_names - def subscription_updated(self, invoice): - self.update_subscription_period() - def process(self): """ To be called by task periodically. It checks the subscription and takes appropriate action diff --git a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py index a0f940098b..d02b4c827c 100644 --- a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py @@ -119,7 +119,7 @@ class TestSubscriptions(unittest.TestCase): subscription.process() self.assertEqual(len(subscription.invoices), 1) - self.assertEqual(subscription.current_invoice_start, nowdate()) + self.assertEqual(subscription.current_invoice_start, '2018-01-01') self.assertEqual(subscription.status, 'Past Due Date') subscription.delete() From 328adedbc889e0103d869b5ea8d169b5b219e06e Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Thu, 1 Mar 2018 08:45:01 +0100 Subject: [PATCH 43/73] adds `start` field to record subscrption start date --- .../doctype/subscriptions/subscriptions.json | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.json b/erpnext/accounts/doctype/subscriptions/subscriptions.json index b2a5af6a3f..44454e109a 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.json +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.json @@ -77,6 +77,37 @@ "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, @@ -625,7 +656,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-02-26 20:34:47.418645", + "modified": "2018-03-01 08:24:07.659772", "modified_by": "Administrator", "module": "Accounts", "name": "Subscriptions", From a93fa16bce8797c0b193f66edf3fb350f5b4f189 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Thu, 1 Mar 2018 08:45:56 +0100 Subject: [PATCH 44/73] freeze screen while waiting for updates on subscription --- erpnext/accounts/doctype/subscriptions/subscriptions.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.js b/erpnext/accounts/doctype/subscriptions/subscriptions.js index ae572d24b7..92cb93fcc5 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.js +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.js @@ -67,6 +67,7 @@ frappe.ui.form.on('Subscriptions', { method: "erpnext.accounts.doctype.subscriptions.subscriptions.get_subscription_updates", args: {name: doc.name}, + freeze: true, callback: function(data){ if(!data.exc){ frm.reload_doc(); From 2a11c18383b4e92427cc8be8f11227a030645a23 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Thu, 1 Mar 2018 08:47:38 +0100 Subject: [PATCH 45/73] set initial invoice period based on subscription start --- erpnext/accounts/doctype/subscriptions/subscriptions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.py b/erpnext/accounts/doctype/subscriptions/subscriptions.py index a56a2f7467..7a4886bafe 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.py @@ -12,7 +12,7 @@ from frappe import _ class Subscriptions(Document): def before_insert(self): # update start just before the subscription doc is created - self.update_subscription_period() + self.update_subscription_period(self.start) def update_subscription_period(self, date=None): self.set_current_invoice_start(date) @@ -95,6 +95,7 @@ class Subscriptions(Document): elif self.is_new_subscription(): self.status = 'Active' # todo: then generate new invoice + self.save() def is_trialling(self): return not self.period_has_passed(self.trial_period_end) and self.is_new_subscription() From 7eabbd93e19c9bc22eb9b547fcd81b5cd7d812cd Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Thu, 1 Mar 2018 09:09:23 +0100 Subject: [PATCH 46/73] update start date alone after restarting subscription --- erpnext/accounts/doctype/subscriptions/subscriptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.py b/erpnext/accounts/doctype/subscriptions/subscriptions.py index 7a4886bafe..1aa1597464 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.py @@ -277,7 +277,7 @@ class Subscriptions(Document): subscription but new trial periods will not be allowed. """ self.status = 'Active' - self.cancelation_date = None + self.start = nowdate() self.update_subscription_period(nowdate()) self.invoices = [] self.save() From b91ec79494921de04c5a73a6f831308b4ec91c09 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Thu, 1 Mar 2018 10:47:00 +0100 Subject: [PATCH 47/73] update tests and more tweaks --- .../doctype/subscriptions/subscriptions.py | 17 +++++++--- .../subscriptions/test_subscriptions.py | 32 ++++++------------- 2 files changed, 21 insertions(+), 28 deletions(-) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.py b/erpnext/accounts/doctype/subscriptions/subscriptions.py index 1aa1597464..ec7d1bba24 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.py @@ -228,8 +228,7 @@ class Subscriptions(Document): elif self.status in ['Past Due Date', 'Unpaid']: self.process_for_past_due_date() - if self.status != 'Canceled': - self.save() + self.save() def process_for_active(self): if getdate(nowdate()) > getdate(self.current_invoice_end) and not self.has_outstanding_invoice(): @@ -240,6 +239,14 @@ class Subscriptions(Document): 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): + self.status = 'Canceled' + if not self.cancelation_date: + self.cancelation_date = nowdate() + def process_for_past_due_date(self): current_invoice = self.get_current_invoice() if not current_invoice: @@ -274,10 +281,10 @@ class Subscriptions(Document): def restart_subscription(self): """ This sets the subscription as active. The subscription will be made to be like a new - subscription but new trial periods will not be allowed. + subscription. """ self.status = 'Active' - self.start = nowdate() + self.db_set('start', nowdate()) self.update_subscription_period(nowdate()) self.invoices = [] self.save() @@ -298,4 +305,4 @@ def restart_subscription(name): @frappe.whitelist() def get_subscription_updates(name): subscription = frappe.get_doc('Subscriptions', name) - subscription.process() \ No newline at end of file + subscription.process() diff --git a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py index d02b4c827c..ac5f097283 100644 --- a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py @@ -106,14 +106,11 @@ class TestSubscriptions(unittest.TestCase): 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') - - subscription.set_current_invoice_start('2018-01-01') - subscription.set_current_invoice_end() - self.assertEqual(subscription.current_invoice_start, '2018-01-01') self.assertEqual(subscription.current_invoice_end, '2018-01-31') subscription.process() @@ -127,9 +124,8 @@ class TestSubscriptions(unittest.TestCase): subscription = frappe.new_doc('Subscriptions') subscription.subscriber = '_Test Customer' subscription.append('plans', {'plan': '_Test Plan Name'}) + subscription.start = '2018-01-01' subscription.insert() - subscription.set_current_invoice_start('2018-01-01') - subscription.set_current_invoice_end() subscription.process() # generate first invoice self.assertEqual(len(subscription.invoices), 1) self.assertEqual(subscription.status, 'Past Due Date') @@ -158,9 +154,8 @@ class TestSubscriptions(unittest.TestCase): subscription = frappe.new_doc('Subscriptions') subscription.subscriber = '_Test Customer' subscription.append('plans', {'plan': '_Test Plan Name'}) + subscription.start = '2018-01-01' subscription.insert() - subscription.set_current_invoice_start('2018-01-01') - subscription.set_current_invoice_end() subscription.process() # generate first invoice self.assertEqual(subscription.status, 'Past Due Date') @@ -182,9 +177,8 @@ class TestSubscriptions(unittest.TestCase): subscription = frappe.new_doc('Subscriptions') subscription.subscriber = '_Test Customer' subscription.append('plans', {'plan': '_Test Plan Name'}) + subscription.start = '2018-01-01' subscription.insert() - subscription.set_current_invoice_start('2018-01-01') - subscription.set_current_invoice_end() subscription.process() # generate first invoice self.assertEqual(subscription.status, 'Past Due Date') @@ -202,9 +196,8 @@ class TestSubscriptions(unittest.TestCase): subscription.subscriber = '_Test Customer' subscription.append('plans', {'plan': '_Test Plan Name'}) subscription.days_until_due = 10 + subscription.start = get_datetime_str(add_to_date(nowdate(), months=-1)) subscription.insert() - subscription.set_current_invoice_start(get_datetime_str(add_to_date(nowdate(), months=-1))) - subscription.set_current_invoice_end() subscription.process() # generate first invoice self.assertEqual(len(subscription.invoices), 1) self.assertEqual(subscription.status, 'Active') @@ -220,9 +213,8 @@ class TestSubscriptions(unittest.TestCase): subscription = frappe.new_doc('Subscriptions') subscription.subscriber = '_Test Customer' subscription.append('plans', {'plan': '_Test Plan Name'}) + subscription.start = '2018-01-01' subscription.insert() - subscription.set_current_invoice_start('2018-01-01') - subscription.set_current_invoice_end() subscription.process() # generate first invoice self.assertEqual(subscription.status, 'Past Due Date') @@ -287,9 +279,8 @@ class TestSubscriptions(unittest.TestCase): subscription = frappe.new_doc('Subscriptions') subscription.subscriber = '_Test Customer' subscription.append('plans', {'plan': '_Test Plan Name'}) + subscription.start = '2018-01-01' subscription.insert() - subscription.set_current_invoice_start('2018-01-01') - subscription.set_current_invoice_end() subscription.process() # generate first invoice invoices = len(subscription.invoices) @@ -322,9 +313,8 @@ class TestSubscriptions(unittest.TestCase): subscription = frappe.new_doc('Subscriptions') subscription.subscriber = '_Test Customer' subscription.append('plans', {'plan': '_Test Plan Name'}) + subscription.start = '2018-01-01' subscription.insert() - subscription.set_current_invoice_start('2018-01-01') - subscription.set_current_invoice_end() subscription.process() # generate first invoice self.assertEqual(subscription.status, 'Past Due Date') @@ -360,9 +350,8 @@ class TestSubscriptions(unittest.TestCase): subscription = frappe.new_doc('Subscriptions') subscription.subscriber = '_Test Customer' subscription.append('plans', {'plan': '_Test Plan Name'}) + subscription.start = '2018-01-01' subscription.insert() - subscription.set_current_invoice_start('2018-01-01') - subscription.set_current_invoice_end() subscription.process() # generate first invoice self.assertEqual(subscription.status, 'Past Due Date') @@ -384,6 +373,3 @@ class TestSubscriptions(unittest.TestCase): settings.cancel_after_grace = default_grace_period_action settings.save() subscription.delete() - - def test_subscription_creation_with_multiple_plans(self): - pass From 78fb12663a08d8757bf936ab951d2973f7a54799 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Thu, 1 Mar 2018 11:27:48 +0100 Subject: [PATCH 48/73] fix test --- erpnext/accounts/doctype/subscriptions/test_subscriptions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py index ac5f097283..3f8f917cba 100644 --- a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe import unittest -from frappe.utils.data import nowdate, add_days, get_last_day, cint, getdate, add_to_date, get_datetime_str +from frappe.utils.data import nowdate, add_days, get_last_day, cint, getdate, add_to_date, get_datetime_str, add_months from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry @@ -196,7 +196,7 @@ class TestSubscriptions(unittest.TestCase): subscription.subscriber = '_Test Customer' subscription.append('plans', {'plan': '_Test Plan Name'}) subscription.days_until_due = 10 - subscription.start = get_datetime_str(add_to_date(nowdate(), months=-1)) + subscription.start = add_months(nowdate(), -1) subscription.insert() subscription.process() # generate first invoice self.assertEqual(len(subscription.invoices), 1) From 0ec445214cb13c04a6289406086776f4fe031f5b Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Thu, 1 Mar 2018 11:33:03 +0100 Subject: [PATCH 49/73] hook to hourly scheduler --- .../doctype/subscriptions/subscriptions.py | 19 +++++++++++++++++++ erpnext/hooks.py | 3 ++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.py b/erpnext/accounts/doctype/subscriptions/subscriptions.py index ec7d1bba24..7d11ad53f1 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.py @@ -290,6 +290,25 @@ class Subscriptions(Document): self.save() +def process_all(): + subscriptions = get_all_subscriptions() + for subscription in subscriptions: + process(subscription) + + +def get_all_subscriptions(): + return frappe.db.sql( + 'select name from `tabSubscriptions` where status != "Canceled"', + as_dict=1 + ) + + +def process(data): + if data: + subscription = frappe.get_doc('Subscriptions', data['name']) + subscription.process() + + @frappe.whitelist() def cancel_subscription(name): subscription = frappe.get_doc('Subscriptions', name) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index a2f7e5667f..999f9aabdf 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -218,7 +218,8 @@ doc_events = { 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", From 36c18c913e62605602900f118cbcea23fbc12c37 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Thu, 1 Mar 2018 16:44:10 +0100 Subject: [PATCH 50/73] docstrings --- .../doctype/subscriptions/subscriptions.py | 136 +++++++++++++++++- 1 file changed, 133 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.py b/erpnext/accounts/doctype/subscriptions/subscriptions.py index 7d11ad53f1..8f5c4f4e47 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.py @@ -15,10 +15,24 @@ class Subscriptions(Document): 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: @@ -27,6 +41,16 @@ class Subscriptions(Document): 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: @@ -37,13 +61,26 @@ class Subscriptions(Document): 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() def validate_plans_billing_cycle(self, 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` ' @@ -55,6 +92,11 @@ class Subscriptions(Document): 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) @@ -78,11 +120,20 @@ class Subscriptions(Document): 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(): @@ -98,9 +149,15 @@ class Subscriptions(Document): 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() def period_has_passed(self, end_date): + """ + Returns true if the given `end_date` has passed + """ # todo: test for illegal time if not end_date: return True @@ -109,6 +166,9 @@ class Subscriptions(Document): 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') @@ -117,6 +177,9 @@ class Subscriptions(Document): 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() @@ -126,6 +189,9 @@ class Subscriptions(Document): 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): @@ -135,6 +201,9 @@ class Subscriptions(Document): frappe.throw(_('Invoice {0} no longer exists'.format(invoice.invoice))) def is_new_subscription(self): + """ + Returns `True` if `Subscription` has never generated an invoice + """ return len(self.invoices) == 0 def validate(self): @@ -142,6 +211,9 @@ class Subscriptions(Document): 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')) @@ -154,6 +226,10 @@ class Subscriptions(Document): self.set_subscription_status() def generate_invoice(self): + """ + Creates a `Sales Invoice` for the `Subscription`, updates `self.invoices` and + saves the `Subscription`. + """ invoice = self.create_invoice() self.append('invoices', {'invoice': invoice.name}) self.save() @@ -161,6 +237,9 @@ class Subscriptions(Document): return invoice def create_invoice(self): + """ + 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 @@ -203,9 +282,15 @@ class Subscriptions(Document): return invoice def get_customer(self, subscriber_name): + """ + Returns the `Customer` linked to the `Subscriber` + """ return frappe.get_value('Subscriber', subscriber_name) def get_items_from_plans(self, plans): + """ + Returns the `Item`s linked to `Subscription Plan` + """ plan_items = [plan.plan for plan in plans] if plan_items: @@ -218,10 +303,9 @@ class Subscriptions(Document): def process(self): """ To be called by task periodically. It checks the subscription and takes appropriate action - as need be. It calls these methods in this order: + as need be. It calls either of these methods depending the `Subscription` status: 1. `process_for_active` 2. `process_for_past_due` - 3. """ if self.status == 'Active': self.process_for_active() @@ -231,6 +315,14 @@ class Subscriptions(Document): 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(): @@ -243,11 +335,22 @@ class Subscriptions(Document): 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))) @@ -259,9 +362,15 @@ class Subscriptions(Document): self.set_status_grace_period() def is_not_outstanding(self, 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 @@ -281,7 +390,8 @@ class Subscriptions(Document): def restart_subscription(self): """ This sets the subscription as active. The subscription will be made to be like a new - subscription. + subscription and the `Subscription` will lose all the history of generated invoices + it has. """ self.status = 'Active' self.db_set('start', nowdate()) @@ -291,12 +401,18 @@ class Subscriptions(Document): 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 @@ -304,6 +420,9 @@ def get_all_subscriptions(): def process(data): + """ + Checks a `Subscription` and updates it status as necessary + """ if data: subscription = frappe.get_doc('Subscriptions', data['name']) subscription.process() @@ -311,17 +430,28 @@ def process(data): @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() From ca5cf35a99bad1fd93a51f687e06ae0c04ff7974 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Thu, 1 Mar 2018 17:06:39 +0100 Subject: [PATCH 51/73] fix: task should explicitly call `commit` and rollback if error --- .../accounts/doctype/subscriptions/subscriptions.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.py b/erpnext/accounts/doctype/subscriptions/subscriptions.py index 8f5c4f4e47..ac0310ae10 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.py @@ -424,8 +424,15 @@ def process(data): Checks a `Subscription` and updates it status as necessary """ if data: - subscription = frappe.get_doc('Subscriptions', data['name']) - subscription.process() + 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() @frappe.whitelist() From 8670c85827a0878b532b1478c8c01d6ff44ca426 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Thu, 1 Mar 2018 17:11:14 +0100 Subject: [PATCH 52/73] make sure default `apply_discount_on` is set when creating invoice --- erpnext/accounts/doctype/subscriptions/subscriptions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.py b/erpnext/accounts/doctype/subscriptions/subscriptions.py index ac0310ae10..bbf2a69a2d 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.py @@ -269,6 +269,8 @@ class Subscriptions(Document): # Discounts if self.apply_additional_discount: invoice.apply_discount_on = self.apply_additional_discount + else: + invoice.apply_discount_on = 'Grand Total' if self.additional_discount_percentage: invoice.additional_discount_percentage = self.additional_discount_percentage From 9df4f0bd43df1c5477d8937266c6aa6600da8288 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Thu, 1 Mar 2018 17:12:43 +0100 Subject: [PATCH 53/73] fix: apply_discount_on field should be of type Select not Data --- erpnext/accounts/doctype/subscriptions/subscriptions.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.json b/erpnext/accounts/doctype/subscriptions/subscriptions.json index 44454e109a..90949e3e78 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.json +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.json @@ -462,7 +462,7 @@ "collapsible": 0, "columns": 0, "fieldname": "apply_additional_discount", - "fieldtype": "Data", + "fieldtype": "Select", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -656,7 +656,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-03-01 08:24:07.659772", + "modified": "2018-03-01 17:12:11.105074", "modified_by": "Administrator", "module": "Accounts", "name": "Subscriptions", From 16afa8765e41c2dd59bb2b07734f6cfc73cb7c4e Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Thu, 1 Mar 2018 17:15:46 +0100 Subject: [PATCH 54/73] tiny refactor of invoice setting logic --- erpnext/accounts/doctype/subscriptions/subscriptions.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.py b/erpnext/accounts/doctype/subscriptions/subscriptions.py index bbf2a69a2d..0a2eea3e3f 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.py @@ -267,17 +267,16 @@ class Subscriptions(Document): ) # Discounts - if self.apply_additional_discount: - invoice.apply_discount_on = self.apply_additional_discount - else: - invoice.apply_discount_on = 'Grand Total' - 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() From 4499e5351c2139d98bda1555d2874b6860de37d6 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Fri, 2 Mar 2018 06:51:33 +0100 Subject: [PATCH 55/73] add article --- .../docs/assets/img/articles/subscriber.png | Bin 0 -> 7820 bytes .../assets/img/articles/subscription-1.png | Bin 0 -> 55322 bytes .../assets/img/articles/subscription-plan.png | Bin 0 -> 34495 bytes .../img/articles/subscription-settings.png | Bin 0 -> 30599 bytes ...ow-to-manage-subscriptions-with-erpnext.md | 104 ++++++++++++++++++ 5 files changed, 104 insertions(+) create mode 100644 erpnext/docs/assets/img/articles/subscriber.png create mode 100644 erpnext/docs/assets/img/articles/subscription-1.png create mode 100644 erpnext/docs/assets/img/articles/subscription-plan.png create mode 100644 erpnext/docs/assets/img/articles/subscription-settings.png create mode 100644 erpnext/docs/user/manual/en/accounts/articles/how-to-manage-subscriptions-with-erpnext.md diff --git a/erpnext/docs/assets/img/articles/subscriber.png b/erpnext/docs/assets/img/articles/subscriber.png new file mode 100644 index 0000000000000000000000000000000000000000..e4ce64d4402b08460c1eac0d42998bda66ec96e4 GIT binary patch literal 7820 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 PJCL=-1@o%2J~#dc_(HBa literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..cbff30f906c223a30f97f3d4bc31ea860f1e5f16 GIT binary patch literal 55322 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b60f796c2cc50be34f82a43fd1e1752f7c0d4178 GIT binary patch literal 34495 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..405f0bf1a2de9f6816577009c8dc1e8e7233d4f6 GIT binary patch literal 30599 zcmd43cT`i`*EXuhV?)s+DpCZLA_xIgnsfyt0@7=!3WQ!12))=qArz^RE|J~|y^A0L zB7`1_5b2Nr5kd$A0^dfD@B4el{l>WW8{>{~FMpWLHhb;4=9+7+=XvH@glTK4Ffy<) z9654?QT5RS-6Kc-h95a{H2BmB;7(ViYR8cy*N&(@xUcV%x-z-zanbt~iB2!vhLX?Z zlW*lO`kVvz#_?4>%{$hn`9c2?R5>*B##P5t4a%deC*8hY9Qn~2!7q8Pqy6t&PtRWW ziT%e|>_zL3lmKwG^B4-&QaOVqzjhVQhmpako?QbCv5d@P9wd%i7Hid{NYsyCXW; zy?#jcZh=amq|zyfp6{Kz1&dL2;RhwWZ~ob^3PHX5N2&8n%DeSX>9VP8=^ylOT^gC0 z;lBQ+DF1Ker;L$Wjse|}b$k-gwngf%*ZV#t&ql-j?%YDYd#b1JORvg-UEq<5Ogw%XZT;e`A1hE zn&y1@AWiku^{mFzcT>an*FG>)_u!i}BBpyLJy>URRE6Pj&!wt0Xad;9MX|(KH9kqm zD7a`l3h)xDJnrt%!cEm{*WX-XfcAH&UbE>Wp6Qmnou6A>9!s(wc48mw+sP(y zMX8g(cT1}DtL47?7@v-z!zRX3@D@?9b{}Z@fZ9j61P)_;m@p*`bX3xdiT_E0!fD<& z@0Bt^<1DA&aBt9F`^nwnMi;3!-+Od{$3!>a<@=Sv3gDj7(+8a=qSjSc!>@qDSTcr~ z%??oA$YH^7ru@y`|BaE41f1=CtaPIwMqPY5wkuH(z{iGr@)wj0`Qa_wUkehzzz4XM zqIWpdzEA&W4cG@QT}-5@1gt()TO^B>??Sh&J%`W3jZEF#R3C&wzLOy=MWc6K!Huui#iEkTgU~p13Vn>O-!)ymEDGx>*eAFsk z^HjeK#PF@K)a`+_YfUK4h0Qa>s1hVNA7FbE8l%V(Kd6wsxHhl&%agTao^Ip}DjkDV zG)q{UgwIbuYJ?>r&e0#!uv4Q4y=Na`1IrJB_U*cn0k^L_JdZGAIWFPTWH1a{b#!tw zfSDJTMEor_Vm(+@T|N2|R&TzlCBMVveRipPXJlKHSn4^yUi7{7o2BD@M*O={H#vb8 zOpegv#Aes>!W^+gK9XK|Zw8j{;Bn0TguI_Sv2boSBef-WTEfu%g3_bx*tOF)*qD?&6ZidcS4tr{HM3uFJ~Ay&<(Qv>WKOyQ5J`+q z_6pU){qJS#QdN$5Nw>kvQF+)L^s#bQ4f*Pu(#j)z2I6HObxc<%^2&Pd+q3)>=@GVtmQj&Jte>T{_vEO{eO<#B{#A2>9?;4)dqZ5F4X?gCgZXF zfe`vvUB>KMKPi~(U!@@G>ZOy7;pU-zY9=T)5%_etU*3BHo}uo}Q~c)FeZ)yy_8rRw zOLv(C;f-RS2&`URb}a4_Zz=XZ7R{H1;0zhqySt=i(cq{wsi9N{%>?ke$NS`MX>durZUBQObjb>>DhEyjxS_ zbnwv($A$tuAWM`u4JNEV*+&WNA1zS`G*}!j?Ff#65n?c#n4j1*$A+nFA!?4e)P}}N z8+}Zp%JSghVyR->rx`9|0Kl-TroOoBy&+Y)uaH8!ab+&-1H(UUrR6W5QnYq2hrgLa zMHko&rGXT`WWN6rnpx&1xmEgg-#(za@57D23#>6^N*ze%Pe&se&lS3L3kBbet=D

eaBkbq+g-9@w0!_R?$68}GoT+;r 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 Kjk$IA>Hh+grLS-R literal 0 HcmV?d00001 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 0000000000..97e6638f49 --- /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. + +Subscriber form + +### 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. + +Subscription Plan Form + +### 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. + +Subscription Settings Form + +## Creating A Subscription +To create a Subscription, go to the Subscription creation form +`Explore > Accounts > Subscriptions` + +Subscription form + +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 From 688fd6b205c860730b1e3e1b7a7df4ddf317fb21 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Fri, 2 Mar 2018 08:54:05 +0100 Subject: [PATCH 56/73] prorate invoice when cancelling subscription --- .../doctype/subscriptions/subscriptions.py | 35 +++++++++++++++---- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.py b/erpnext/accounts/doctype/subscriptions/subscriptions.py index 0a2eea3e3f..23bb05da2b 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.py @@ -225,18 +225,18 @@ class Subscriptions(Document): # todo: deal with users who collect prepayments. Maybe a new Subscription Invoice doctype? self.set_subscription_status() - def generate_invoice(self): + def generate_invoice(self, prorate=0): """ Creates a `Sales Invoice` for the `Subscription`, updates `self.invoices` and saves the `Subscription`. """ - invoice = self.create_invoice() + invoice = self.create_invoice(prorate) self.append('invoices', {'invoice': invoice.name}) self.save() return invoice - def create_invoice(self): + def create_invoice(self, prorate): """ Creates a `Sales Invoice`, submits it and returns it """ @@ -247,7 +247,7 @@ class Subscriptions(Document): # 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) + items_list = self.get_items_from_plans(self.plans, prorate) for item in items_list: item['qty'] = self.quantity invoice.append('items', item) @@ -288,18 +288,35 @@ class Subscriptions(Document): """ return frappe.get_value('Subscriber', subscriber_name) - def get_items_from_plans(self, plans): + 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: + 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 ) - return item_names + + elif plan_items: + prorate_factor = self.get_proration_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 get_proration_factor(self, 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 def process(self): """ @@ -384,8 +401,12 @@ class Subscriptions(Document): 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): From 49d34df8f453ab4cb59a74cc24193ff09377e5db Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Fri, 2 Mar 2018 10:16:57 +0100 Subject: [PATCH 57/73] more tests --- .../subscriptions/test_subscriptions.py | 73 ++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py index 3f8f917cba..b2975d7a07 100644 --- a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe import unittest -from frappe.utils.data import nowdate, add_days, get_last_day, cint, getdate, add_to_date, get_datetime_str, add_months +from frappe.utils.data import nowdate, add_days, get_last_day, cint, getdate, add_to_date, get_datetime_str, add_months, date_diff from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry @@ -270,6 +270,77 @@ class TestSubscriptions(unittest.TestCase): subscription.delete() + def test_subscription_cancelation_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( + subscription.get_proration_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_proration_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_proration_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_cancelation_and_process(self): settings = frappe.get_single('Subscription Settings') default_grace_period_action = settings.cancel_after_grace From c15dc21b7d0e553a95c44b8f6768dd343ca04b53 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Fri, 2 Mar 2018 11:00:15 +0100 Subject: [PATCH 58/73] clean up --- .../doctype/subscriptions/subscriptions.py | 39 +++++++++------- .../subscriptions/test_subscriptions.py | 44 +++++++++---------- 2 files changed, 44 insertions(+), 39 deletions(-) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.py b/erpnext/accounts/doctype/subscriptions/subscriptions.py index 23bb05da2b..2fd2fd6edc 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.py @@ -3,10 +3,11 @@ # For license information, please see license.txt from __future__ import unicode_literals + import frappe -from frappe.model.document import Document -from frappe.utils.data import now, nowdate, getdate, cint, add_days, date_diff, get_last_day, get_first_day, add_to_date 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): @@ -67,7 +68,8 @@ class Subscriptions(Document): """ return self.get_billing_cycle_data() - def validate_plans_billing_cycle(self, 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 @@ -112,7 +114,7 @@ class Subscriptions(Document): elif interval == 'Month': data['months'] = interval_count elif interval == 'Year': - data['years'] == interval_count + data['years'] = interval_count # todo: test week elif interval == 'Week': data['days'] = interval_count * 7 - 1 @@ -154,7 +156,8 @@ class Subscriptions(Document): """ return not self.period_has_passed(self.trial_period_end) and self.is_new_subscription() - def period_has_passed(self, end_date): + @staticmethod + def period_has_passed(end_date): """ Returns true if the given `end_date` has passed """ @@ -198,7 +201,7 @@ class Subscriptions(Document): doc = frappe.get_doc('Sales Invoice', current.invoice) return doc else: - frappe.throw(_('Invoice {0} no longer exists'.format(invoice.invoice))) + frappe.throw(_('Invoice {0} no longer exists'.format(current.invoice))) def is_new_subscription(self): """ @@ -282,7 +285,8 @@ class Subscriptions(Document): return invoice - def get_customer(self, subscriber_name): + @staticmethod + def get_customer(subscriber_name): """ Returns the `Customer` linked to the `Subscriber` """ @@ -302,7 +306,7 @@ class Subscriptions(Document): ) elif plan_items: - prorate_factor = self.get_proration_factor(self.current_invoice_end, self.current_invoice_start) + 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', @@ -311,13 +315,6 @@ class Subscriptions(Document): return item_names - def get_proration_factor(self, 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 - def process(self): """ To be called by task periodically. It checks the subscription and takes appropriate action @@ -379,7 +376,8 @@ class Subscriptions(Document): else: self.set_status_grace_period() - def is_not_outstanding(self, invoice): + @staticmethod + def is_not_outstanding(invoice): """ Return `True` if the given invoice is paid """ @@ -394,7 +392,6 @@ class Subscriptions(Document): return False else: return not self.is_not_outstanding(current_invoice) - return True def cancel_subscription(self): """ @@ -457,6 +454,14 @@ def process(data): 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): """ diff --git a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py index b2975d7a07..627ebddb72 100644 --- a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py @@ -3,10 +3,11 @@ # See license.txt from __future__ import unicode_literals -import frappe import unittest -from frappe.utils.data import nowdate, add_days, get_last_day, cint, getdate, add_to_date, get_datetime_str, add_months, date_diff -from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry + +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): @@ -156,7 +157,7 @@ class TestSubscriptions(unittest.TestCase): subscription.append('plans', {'plan': '_Test Plan Name'}) subscription.start = '2018-01-01' subscription.insert() - subscription.process() # generate first invoice + subscription.process() # generate first invoice self.assertEqual(subscription.status, 'Past Due Date') @@ -179,7 +180,7 @@ class TestSubscriptions(unittest.TestCase): subscription.append('plans', {'plan': '_Test Plan Name'}) subscription.start = '2018-01-01' subscription.insert() - subscription.process() # generate first invoice + subscription.process() # generate first invoice self.assertEqual(subscription.status, 'Past Due Date') @@ -198,13 +199,13 @@ class TestSubscriptions(unittest.TestCase): subscription.days_until_due = 10 subscription.start = add_months(nowdate(), -1) subscription.insert() - subscription.process() # generate first invoice + subscription.process() # generate first invoice self.assertEqual(len(subscription.invoices), 1) self.assertEqual(subscription.status, 'Active') subscription.delete() - def test_subcription_is_past_due_doesnt_change_within_grace_period(self): + 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 @@ -215,7 +216,7 @@ class TestSubscriptions(unittest.TestCase): subscription.append('plans', {'plan': '_Test Plan Name'}) subscription.start = '2018-01-01' subscription.insert() - subscription.process() # generate first invoice + subscription.process() # generate first invoice self.assertEqual(subscription.status, 'Past Due Date') @@ -238,20 +239,20 @@ class TestSubscriptions(unittest.TestCase): subscription.subscriber = '_Test Customer' subscription.append('plans', {'plan': '_Test Plan Name'}) subscription.save() - subscription.process() # no changes expected + 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 + 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 + 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)) @@ -259,7 +260,7 @@ class TestSubscriptions(unittest.TestCase): subscription.delete() - def test_subcription_cancelation(self): + def test_subscription_cancelation(self): subscription = frappe.new_doc('Subscriptions') subscription.subscriber = '_Test Customer' subscription.append('plans', {'plan': '_Test Plan Name'}) @@ -270,7 +271,7 @@ class TestSubscriptions(unittest.TestCase): subscription.delete() - def test_subscription_cancelation_invoices(self): + def test_subscription_cancellation_invoices(self): subscription = frappe.new_doc('Subscriptions') subscription.subscriber = '_Test Customer' subscription.append('plans', {'plan': '_Test Plan Name'}) @@ -288,16 +289,15 @@ class TestSubscriptions(unittest.TestCase): prorate_factor = diff/plan_days self.assertEqual( - subscription.get_proration_factor( - subscription.current_invoice_end, - subscription.current_invoice_start - ), prorate_factor) + 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_proration_false(self): + def test_subscription_cancellation_invoices_with_prorata_false(self): settings = frappe.get_single('Subscription Settings') to_prorate = settings.prorate settings.prorate = 0 @@ -317,7 +317,7 @@ class TestSubscriptions(unittest.TestCase): subscription.delete() - def test_subscription_cancellation_invoices_with_proration_true(self): + def test_subscription_cancellation_invoices_with_prorata_true(self): settings = frappe.get_single('Subscription Settings') to_prorate = settings.prorate settings.prorate = 1 @@ -341,7 +341,7 @@ class TestSubscriptions(unittest.TestCase): subscription.delete() - def test_subcription_cancelation_and_process(self): + 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 @@ -386,7 +386,7 @@ class TestSubscriptions(unittest.TestCase): subscription.append('plans', {'plan': '_Test Plan Name'}) subscription.start = '2018-01-01' subscription.insert() - subscription.process() # generate first invoice + subscription.process() # generate first invoice self.assertEqual(subscription.status, 'Past Due Date') @@ -423,7 +423,7 @@ class TestSubscriptions(unittest.TestCase): subscription.append('plans', {'plan': '_Test Plan Name'}) subscription.start = '2018-01-01' subscription.insert() - subscription.process() # generate first invoice + subscription.process() # generate first invoice self.assertEqual(subscription.status, 'Past Due Date') From e13570f343f411f56ab8b965cdfb442ef85476bb Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Fri, 2 Mar 2018 12:27:12 +0100 Subject: [PATCH 59/73] codacy review --- .../accounts/doctype/subscriber/subscriber.js | 6 -- .../accounts/doctype/subscriber/subscriber.py | 1 - .../doctype/subscriber/test_subscriber.py | 1 - .../subscription_invoice.js | 6 -- .../subscription_invoice.py | 1 - .../test_subscription_invoice.py | 1 - .../subscription_plan/subscription_plan.js | 6 -- .../test_subscription_plan.py | 1 - .../subscription_plan_detail.py | 1 - .../subscription_settings.js | 6 -- .../subscription_settings.py | 1 - .../test_subscription_settings.py | 1 - .../doctype/subscriptions/subscriptions.py | 22 +++-- .../subscriptions/test_subscriptions.py | 81 ++++++++++--------- 14 files changed, 52 insertions(+), 83 deletions(-) diff --git a/erpnext/accounts/doctype/subscriber/subscriber.js b/erpnext/accounts/doctype/subscriber/subscriber.js index ba5cdf97f0..f5ea8047c6 100644 --- a/erpnext/accounts/doctype/subscriber/subscriber.js +++ b/erpnext/accounts/doctype/subscriber/subscriber.js @@ -1,8 +1,2 @@ // 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.py b/erpnext/accounts/doctype/subscriber/subscriber.py index c0aabcfbc9..03eb0f5ebb 100644 --- a/erpnext/accounts/doctype/subscriber/subscriber.py +++ b/erpnext/accounts/doctype/subscriber/subscriber.py @@ -3,7 +3,6 @@ # For license information, please see license.txt from __future__ import unicode_literals -import frappe from frappe.model.document import Document class Subscriber(Document): diff --git a/erpnext/accounts/doctype/subscriber/test_subscriber.py b/erpnext/accounts/doctype/subscriber/test_subscriber.py index e8684c3ff1..3e2fc07c7b 100644 --- a/erpnext/accounts/doctype/subscriber/test_subscriber.py +++ b/erpnext/accounts/doctype/subscriber/test_subscriber.py @@ -3,7 +3,6 @@ # See license.txt from __future__ import unicode_literals -import frappe import unittest class TestSubscriber(unittest.TestCase): diff --git a/erpnext/accounts/doctype/subscription_invoice/subscription_invoice.js b/erpnext/accounts/doctype/subscription_invoice/subscription_invoice.js index 40f9af303e..f5ea8047c6 100644 --- a/erpnext/accounts/doctype/subscription_invoice/subscription_invoice.js +++ b/erpnext/accounts/doctype/subscription_invoice/subscription_invoice.js @@ -1,8 +1,2 @@ // 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.py b/erpnext/accounts/doctype/subscription_invoice/subscription_invoice.py index 69ff3e54c1..6f459b4790 100644 --- a/erpnext/accounts/doctype/subscription_invoice/subscription_invoice.py +++ b/erpnext/accounts/doctype/subscription_invoice/subscription_invoice.py @@ -3,7 +3,6 @@ # For license information, please see license.txt from __future__ import unicode_literals -import frappe from frappe.model.document import Document class SubscriptionInvoice(Document): diff --git a/erpnext/accounts/doctype/subscription_invoice/test_subscription_invoice.py b/erpnext/accounts/doctype/subscription_invoice/test_subscription_invoice.py index 1d542b0f7d..e60a4eeca9 100644 --- a/erpnext/accounts/doctype/subscription_invoice/test_subscription_invoice.py +++ b/erpnext/accounts/doctype/subscription_invoice/test_subscription_invoice.py @@ -3,7 +3,6 @@ # See license.txt from __future__ import unicode_literals -import frappe import unittest class TestSubscriptionInvoice(unittest.TestCase): diff --git a/erpnext/accounts/doctype/subscription_plan/subscription_plan.js b/erpnext/accounts/doctype/subscription_plan/subscription_plan.js index 9baacdd4ea..f5ea8047c6 100644 --- a/erpnext/accounts/doctype/subscription_plan/subscription_plan.js +++ b/erpnext/accounts/doctype/subscription_plan/subscription_plan.js @@ -1,8 +1,2 @@ // 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/test_subscription_plan.py b/erpnext/accounts/doctype/subscription_plan/test_subscription_plan.py index 4a9b578acc..73afbf620e 100644 --- a/erpnext/accounts/doctype/subscription_plan/test_subscription_plan.py +++ b/erpnext/accounts/doctype/subscription_plan/test_subscription_plan.py @@ -3,7 +3,6 @@ # See license.txt from __future__ import unicode_literals -import frappe import unittest class TestSubscriptionPlan(unittest.TestCase): diff --git a/erpnext/accounts/doctype/subscription_plan_detail/subscription_plan_detail.py b/erpnext/accounts/doctype/subscription_plan_detail/subscription_plan_detail.py index 04ec4afb13..1d9606ff78 100644 --- a/erpnext/accounts/doctype/subscription_plan_detail/subscription_plan_detail.py +++ b/erpnext/accounts/doctype/subscription_plan_detail/subscription_plan_detail.py @@ -3,7 +3,6 @@ # For license information, please see license.txt from __future__ import unicode_literals -import frappe from frappe.model.document import Document class SubscriptionPlanDetail(Document): diff --git a/erpnext/accounts/doctype/subscription_settings/subscription_settings.js b/erpnext/accounts/doctype/subscription_settings/subscription_settings.js index c4541c3719..f5ea8047c6 100644 --- a/erpnext/accounts/doctype/subscription_settings/subscription_settings.js +++ b/erpnext/accounts/doctype/subscription_settings/subscription_settings.js @@ -1,8 +1,2 @@ // 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.py b/erpnext/accounts/doctype/subscription_settings/subscription_settings.py index 3d382a7f5a..cc378e4d9f 100644 --- a/erpnext/accounts/doctype/subscription_settings/subscription_settings.py +++ b/erpnext/accounts/doctype/subscription_settings/subscription_settings.py @@ -3,7 +3,6 @@ # For license information, please see license.txt from __future__ import unicode_literals -import frappe from frappe.model.document import Document class SubscriptionSettings(Document): diff --git a/erpnext/accounts/doctype/subscription_settings/test_subscription_settings.py b/erpnext/accounts/doctype/subscription_settings/test_subscription_settings.py index b9592d3cf4..82c7e1d269 100644 --- a/erpnext/accounts/doctype/subscription_settings/test_subscription_settings.py +++ b/erpnext/accounts/doctype/subscription_settings/test_subscription_settings.py @@ -3,7 +3,6 @@ # See license.txt from __future__ import unicode_literals -import frappe import unittest class TestSubscriptionSettings(unittest.TestCase): diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.py b/erpnext/accounts/doctype/subscriptions/subscriptions.py index 2fd2fd6edc..ed1d3534a2 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.py @@ -29,9 +29,8 @@ class Subscriptions(Document): 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 + 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(): @@ -49,7 +48,7 @@ class Subscriptions(Document): 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 + current billing period where `x` is the billing interval from the `Subscription Plan` in the `Subscription`. """ if self.is_trialling(): @@ -63,7 +62,7 @@ class Subscriptions(Document): def get_billing_cycle(self): """ - Returns a dict containing billing cycle information deduced from the + Returns a dict containing billing cycle information deduced from the `Subscription Plan` in the `Subscription`. """ return self.get_billing_cycle_data() @@ -125,7 +124,7 @@ class Subscriptions(Document): """ 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 + 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') @@ -262,7 +261,7 @@ class Subscriptions(Document): # Due date invoice.append( - 'payment_schedule', + 'payment_schedule', { 'due_date': add_days(self.current_invoice_end, cint(self.days_until_due)), 'invoice_portion': 100 @@ -276,8 +275,7 @@ class Subscriptions(Document): 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: + if not self.apply_additional_discount and (self.additional_discount_percentage or self.additional_discount_amount): self.apply_additional_discount = 'Grand Total' invoice.save() @@ -331,7 +329,7 @@ class Subscriptions(Document): def process_for_active(self): """ - Called by `process` if the status of the `Subscription` is 'Active'. + Called by `process` if the status of the `Subscription` is 'Active'. The possible outcomes of this method are: 1. Generate a new invoice @@ -359,8 +357,8 @@ class Subscriptions(Document): def process_for_past_due_date(self): """ - Called by `process` if the status of the `Subscription` is 'Past Due Date'. - + 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' diff --git a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py index 627ebddb72..ad1eb2113b 100644 --- a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py @@ -10,45 +10,48 @@ from erpnext.accounts.doctype.subscriptions.subscriptions import get_prorata_fac from frappe.utils.data import nowdate, add_days, add_to_date, add_months, date_diff +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.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(): + if not frappe.db.exists('Subscriber', '_Test Customer'): + subscriber = frappe.new_doc('Subscriber') + subscriber.subscriber_name = '_Test Customer' + subscriber.customer = '_Test Customer' + subscriber.insert() + + 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() + create_plan() + create_subscriber() def test_create_subscription_with_trial_with_correct_period(self): subscription = frappe.new_doc('Subscriptions') @@ -161,7 +164,7 @@ class TestSubscriptions(unittest.TestCase): self.assertEqual(subscription.status, 'Past Due Date') - subscription.process() + subscription.process() # This should change status to Canceled since grace period is 0 self.assertEqual(subscription.status, 'Canceled') @@ -245,7 +248,7 @@ class TestSubscriptions(unittest.TestCase): 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()) @@ -358,7 +361,7 @@ class TestSubscriptions(unittest.TestCase): self.assertEqual(subscription.status, 'Past Due Date') self.assertEqual(len(subscription.invoices), invoices) - subscription.cancel_subscription() + subscription.cancel_subscription() self.assertEqual(subscription.status, 'Canceled') self.assertEqual(len(subscription.invoices), invoices) From d43422c18e76c445ced198d2ecd457633c6db806 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Sat, 3 Mar 2018 07:37:59 +0100 Subject: [PATCH 60/73] enhancement: ensure subscription is cancelled before attempting to restart --- .../accounts/doctype/subscriptions/subscriptions.py | 13 ++++++++----- .../doctype/subscriptions/test_subscriptions.py | 10 ++++++++++ 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.py b/erpnext/accounts/doctype/subscriptions/subscriptions.py index ed1d3534a2..6264972a98 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.py @@ -410,11 +410,14 @@ class Subscriptions(Document): 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() + if self.status == 'Canceled': + self.status = 'Active' + self.db_set('start', nowdate()) + self.update_subscription_period(nowdate()) + self.invoices = [] + self.save() + else: + frappe.throw(_('You cannot restart a Subscription that is not cancelled.')) def process_all(): diff --git a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py index ad1eb2113b..4452e631fb 100644 --- a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py @@ -447,3 +447,13 @@ class TestSubscriptions(unittest.TestCase): settings.cancel_after_grace = default_grace_period_action settings.save() subscription.delete() + + def test_restart_active_subscription(self): + subscription = frappe.new_doc('Subscriptions') + subscription.subscriber = '_Test Customer' + subscription.append('plans', {'plan': '_Test Plan Name'}) + subscription.save() + + self.assertRaises(frappe.ValidationError, subscription.restart_subscription) + + subscription.delete() From 79a01561d52326c35dad4362007f0ef2c9d5cfaf Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Sat, 3 Mar 2018 07:39:44 +0100 Subject: [PATCH 61/73] enhancement: do nothing if `cancel_sbscription` is called for cancelled subscription --- .../doctype/subscriptions/subscriptions.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.py b/erpnext/accounts/doctype/subscriptions/subscriptions.py index 6264972a98..7796daa8e3 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.py @@ -396,13 +396,14 @@ class Subscriptions(Document): 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() + if self.status != 'Canceled': + 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): """ From 024c28acf752c790e70fd9bb5f28c54b4c789635 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Sat, 3 Mar 2018 10:32:02 +0100 Subject: [PATCH 62/73] fix proration logic to reduce possibility of rounding errors --- .../doctype/subscriptions/subscriptions.py | 24 ++++++++------ .../subscriptions/test_subscriptions.py | 32 ++++++++++++------- 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.py b/erpnext/accounts/doctype/subscriptions/subscriptions.py index 7796daa8e3..15e4611149 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.py @@ -7,7 +7,7 @@ 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 +from frappe.utils.data import nowdate, getdate, cint, add_days, date_diff, get_last_day, add_to_date, flt class Subscriptions(Document): @@ -304,7 +304,7 @@ class Subscriptions(Document): ) elif plan_items: - prorate_factor = get_prorata_factor(self.current_invoice_end, self.current_invoice_start) + prorate_factor = self.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', @@ -420,6 +420,18 @@ class Subscriptions(Document): else: frappe.throw(_('You cannot restart a Subscription that is not cancelled.')) + def get_prorata_factor(self, period_end, period_start): + diff = flt(date_diff(nowdate(), period_start) + 1) + plan_days = flt(date_diff(period_end, period_start) + 1) + prorate_factor = diff / plan_days + + return prorate_factor + + def get_precision(self): + invoice = self.get_current_invoice() + if invoice: + return invoice.precision('grand_total') + def process_all(): """ @@ -456,14 +468,6 @@ def process(data): 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): """ diff --git a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py index 4452e631fb..fb92d8626f 100644 --- a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py @@ -6,8 +6,7 @@ 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 +from frappe.utils.data import nowdate, add_days, add_to_date, add_months, date_diff, flt def create_plan(): @@ -275,6 +274,11 @@ class TestSubscriptions(unittest.TestCase): subscription.delete() def test_subscription_cancellation_invoices(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'}) @@ -287,18 +291,22 @@ class TestSubscriptions(unittest.TestCase): 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 + diff = flt(date_diff(nowdate(), subscription.current_invoice_start) + 1) + plan_days = flt(date_diff(subscription.current_invoice_end, subscription.current_invoice_start) + 1) + prorate_factor = flt(diff/plan_days) self.assertEqual( - get_prorata_factor(subscription.current_invoice_end, subscription.current_invoice_start), - prorate_factor + flt( + subscription.get_prorata_factor(subscription.current_invoice_end, subscription.current_invoice_start), + 2), + flt(prorate_factor, 2) ) - self.assertEqual(invoice.grand_total, prorate_factor * 900) + self.assertEqual(flt(invoice.grand_total, 2), flt(prorate_factor * 900, 2)) self.assertEqual(subscription.status, 'Canceled') subscription.delete() + settings.prorate = to_prorate + settings.save() def test_subscription_cancellation_invoices_with_prorata_false(self): settings = frappe.get_single('Subscription Settings') @@ -333,11 +341,11 @@ class TestSubscriptions(unittest.TestCase): 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 + diff = flt(date_diff(nowdate(), subscription.current_invoice_start) + 1) + plan_days = flt(date_diff(subscription.current_invoice_end, subscription.current_invoice_start) + 1) + prorate_factor = flt(diff / plan_days) - self.assertEqual(invoice.grand_total, prorate_factor * 900) + self.assertEqual(flt(invoice.grand_total, 2), flt(prorate_factor * 900, 2)) settings.prorate = to_prorate settings.save() From fbdd5d30e9bacb432c255017f996d2b52e8b4b14 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Sat, 3 Mar 2018 11:19:18 +0100 Subject: [PATCH 63/73] more tests and bug fixes --- .../doctype/subscriptions/subscriptions.py | 7 ++-- .../subscriptions/test_subscriptions.py | 32 ++++++++++++++++++- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.py b/erpnext/accounts/doctype/subscriptions/subscriptions.py index 15e4611149..02a30a844c 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.py @@ -273,10 +273,11 @@ class Subscriptions(Document): invoice.additional_discount_percentage = self.additional_discount_percentage if self.additional_discount_amount: - invoice.additional_discount_amount = self.additional_discount_amount + invoice.discount_amount = self.additional_discount_amount - if not self.apply_additional_discount and (self.additional_discount_percentage or self.additional_discount_amount): - self.apply_additional_discount = 'Grand Total' + if self.additional_discount_percentage or self.additional_discount_amount: + discount_on = self.apply_additional_discount + invoice.apply_additional_discount = discount_on if discount_on else 'Grand Total' invoice.save() invoice.submit() diff --git a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py index fb92d8626f..b290f0048b 100644 --- a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py @@ -186,7 +186,7 @@ class TestSubscriptions(unittest.TestCase): self.assertEqual(subscription.status, 'Past Due Date') - subscription.process() + subscription.process() # This should change status to Canceled since grace period is 0 self.assertEqual(subscription.status, 'Unpaid') @@ -465,3 +465,33 @@ class TestSubscriptions(unittest.TestCase): self.assertRaises(frappe.ValidationError, subscription.restart_subscription) subscription.delete() + + def test_subscription_invoice_discount_percentage(self): + subscription = frappe.new_doc('Subscriptions') + subscription.subscriber = '_Test Customer' + subscription.additional_discount_percentage = 10 + subscription.append('plans', {'plan': '_Test Plan Name'}) + subscription.save() + subscription.cancel_subscription() + + invoice = subscription.get_current_invoice() + + self.assertEqual(invoice.additional_discount_percentage, 10) + self.assertEqual(invoice.apply_discount_on, 'Grand Total') + + subscription.delete() + + def test_subscription_invoice_discount_amount(self): + subscription = frappe.new_doc('Subscriptions') + subscription.subscriber = '_Test Customer' + subscription.additional_discount_amount = 11 + subscription.append('plans', {'plan': '_Test Plan Name'}) + subscription.save() + subscription.cancel_subscription() + + invoice = subscription.get_current_invoice() + + self.assertEqual(invoice.discount_amount, 11) + self.assertEqual(invoice.apply_discount_on, 'Grand Total') + + subscription.delete() From 2379451b6875c891e1f56a75f64d79de2aa86bbc Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Sat, 3 Mar 2018 17:18:47 +0100 Subject: [PATCH 64/73] codacy tinzz --- .../doctype/subscriptions/subscriptions.py | 19 ++++++++++--------- .../subscriptions/test_subscriptions.py | 3 ++- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.py b/erpnext/accounts/doctype/subscriptions/subscriptions.py index 02a30a844c..a193e69d67 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.py @@ -305,7 +305,7 @@ class Subscriptions(Document): ) elif plan_items: - prorate_factor = self.get_prorata_factor(self.current_invoice_end, self.current_invoice_start) + 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', @@ -331,7 +331,7 @@ class Subscriptions(Document): 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' @@ -421,19 +421,20 @@ class Subscriptions(Document): else: frappe.throw(_('You cannot restart a Subscription that is not cancelled.')) - def get_prorata_factor(self, period_end, period_start): - diff = flt(date_diff(nowdate(), period_start) + 1) - plan_days = flt(date_diff(period_end, period_start) + 1) - prorate_factor = diff / plan_days - - return prorate_factor - def get_precision(self): invoice = self.get_current_invoice() if invoice: return invoice.precision('grand_total') +def get_prorata_factor(period_end, period_start): + diff = flt(date_diff(nowdate(), period_start) + 1) + plan_days = flt(date_diff(period_end, period_start) + 1) + prorate_factor = diff / plan_days + + return prorate_factor + + def process_all(): """ Task to updates the status of all `Subscription` apart from those that are cancelled diff --git a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py index b290f0048b..5e44197723 100644 --- a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py @@ -6,6 +6,7 @@ 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, flt @@ -297,7 +298,7 @@ class TestSubscriptions(unittest.TestCase): self.assertEqual( flt( - subscription.get_prorata_factor(subscription.current_invoice_end, subscription.current_invoice_start), + get_prorata_factor(subscription.current_invoice_end, subscription.current_invoice_start), 2), flt(prorate_factor, 2) ) From 3b1251ef47a35bce8b83497d556e98cc658cc280 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Sat, 3 Mar 2018 17:36:07 +0100 Subject: [PATCH 65/73] codacy tinzz --- erpnext/accounts/doctype/subscriptions/test_subscriptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py index 5e44197723..75fe384bdd 100644 --- a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py @@ -223,7 +223,7 @@ class TestSubscriptions(unittest.TestCase): self.assertEqual(subscription.status, 'Past Due Date') - subscription.process() + subscription.process() # Grace period is 1000 days so status should remain as Past Due Date self.assertEqual(subscription.status, 'Past Due Date') From ef605d5f00053e4042a2cda4e4aaead054a34a62 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Sat, 3 Mar 2018 18:03:04 +0100 Subject: [PATCH 66/73] codacy --- erpnext/accounts/doctype/subscriptions/test_subscriptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py index 75fe384bdd..8dc8081702 100644 --- a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py @@ -402,7 +402,7 @@ class TestSubscriptions(unittest.TestCase): self.assertEqual(subscription.status, 'Past Due Date') - subscription.process() + subscription.process() self.assertEqual(subscription.status, 'Unpaid') subscription.cancel_subscription() From 458a1fe61ae732c25f8f9d402b294ddef58e5c56 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Sat, 3 Mar 2018 18:11:48 +0100 Subject: [PATCH 67/73] codacy --- erpnext/accounts/doctype/subscriptions/test_subscriptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py index 8dc8081702..75c2bf2a74 100644 --- a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py +++ b/erpnext/accounts/doctype/subscriptions/test_subscriptions.py @@ -439,7 +439,7 @@ class TestSubscriptions(unittest.TestCase): self.assertEqual(subscription.status, 'Past Due Date') - subscription.process() + subscription.process() # This should change status to Canceled since grace period is 0 self.assertEqual(subscription.status, 'Unpaid') From 3d26003c1f8ef1ed4b70bca43bd38be7b86ff586 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Fri, 9 Mar 2018 09:37:06 +0100 Subject: [PATCH 68/73] add column breaks to Subscription --- .../doctype/subscriptions/subscriptions.json | 157 ++++++++++++++---- 1 file changed, 124 insertions(+), 33 deletions(-) diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.json b/erpnext/accounts/doctype/subscriptions/subscriptions.json index 90949e3e78..ae324cfd76 100644 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.json +++ b/erpnext/accounts/doctype/subscriptions/subscriptions.json @@ -264,6 +264,68 @@ "translatable": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "cb_1", + "fieldtype": "Column 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, + "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, @@ -329,6 +391,37 @@ "translatable": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "sb_4", + "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": "Plans", + "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, @@ -487,6 +580,36 @@ "translatable": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "cb_2", + "fieldtype": "Column 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, + "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, @@ -549,38 +672,6 @@ "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, @@ -656,7 +747,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-03-01 17:12:11.105074", + "modified": "2018-03-09 09:31:16.285992", "modified_by": "Administrator", "module": "Accounts", "name": "Subscriptions", From 0125f7b1300d72257ca830b797bb9f0e32a8b8c0 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Mon, 12 Mar 2018 13:50:50 +0100 Subject: [PATCH 69/73] rename to Subscription and transfer present Subscription records to Auto Repeat in frappe --- .../doctype/journal_entry/journal_entry.json | 54 +- .../doctype/payment_entry/payment_entry.json | 8 +- .../doctype/payment_entry/payment_entry.py | 2 +- .../purchase_invoice/purchase_invoice.json | 134 +- .../purchase_invoice/purchase_invoice.py | 2 +- .../doctype/sales_invoice/sales_invoice.json | 2516 +++++++++-------- .../doctype/sales_invoice/sales_invoice.py | 4 +- .../doctype/subscription/subscription.js | 119 +- .../doctype/subscription/subscription.json | 1099 ++++--- .../doctype/subscription/subscription.py | 746 +++-- .../doctype/subscription/subscription_list.js | 19 +- .../doctype/subscription/test_subscription.py | 539 +++- .../doctype/subscriptions/subscriptions.js | 78 - .../doctype/subscriptions/subscriptions.json | 786 ----- .../doctype/subscriptions/subscriptions.py | 499 ---- .../subscriptions/test_subscriptions.js | 23 - .../subscriptions/test_subscriptions.py | 498 ---- .../purchase_order/purchase_order.json | 9 +- .../supplier_quotation.json | 90 +- erpnext/hooks.py | 3 +- erpnext/patches.txt | 2 + .../v10_1}/__init__.py | 0 .../v10_1/drop_old_subscription_records.py | 12 + .../transfer_subscription_to_auto_repeat.py | 31 + erpnext/patches/v8_1/gst_fixes.py | 4 +- .../selling/doctype/quotation/quotation.json | 102 +- .../selling/doctype/quotation/quotation.py | 2 +- .../doctype/sales_order/sales_order.json | 123 +- .../doctype/sales_order/sales_order.py | 8 +- .../doctype/delivery_note/delivery_note.json | 217 +- .../purchase_receipt/purchase_receipt.json | 113 +- .../emails/recurring_document_failed.html | 11 - 32 files changed, 3656 insertions(+), 4197 deletions(-) delete mode 100644 erpnext/accounts/doctype/subscriptions/subscriptions.js delete mode 100644 erpnext/accounts/doctype/subscriptions/subscriptions.json delete mode 100644 erpnext/accounts/doctype/subscriptions/subscriptions.py delete mode 100644 erpnext/accounts/doctype/subscriptions/test_subscriptions.js delete mode 100644 erpnext/accounts/doctype/subscriptions/test_subscriptions.py rename erpnext/{accounts/doctype/subscriptions => patches/v10_1}/__init__.py (100%) create mode 100644 erpnext/patches/v10_1/drop_old_subscription_records.py create mode 100644 erpnext/patches/v10_1/transfer_subscription_to_auto_repeat.py delete mode 100644 erpnext/templates/emails/recurring_document_failed.html diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json index 19f4b56928..4d445a4a79 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.json +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json @@ -40,6 +40,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -71,6 +72,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -104,6 +106,7 @@ "reqd": 1, "search_index": 1, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -136,6 +139,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 1, + "translatable": 0, "unique": 0 }, { @@ -165,6 +169,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "50%" }, @@ -197,6 +202,7 @@ "reqd": 1, "search_index": 1, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -229,6 +235,7 @@ "reqd": 1, "search_index": 1, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -260,6 +267,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -292,6 +300,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -320,6 +329,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -352,6 +362,7 @@ "reqd": 0, "search_index": 1, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -383,6 +394,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -414,6 +426,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -442,6 +455,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -474,6 +488,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -506,6 +521,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -539,6 +555,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -570,6 +587,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -600,6 +618,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -631,6 +650,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -661,6 +681,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -690,6 +711,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -720,6 +742,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -751,6 +774,7 @@ "reqd": 0, "search_index": 1, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -783,6 +807,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -811,6 +836,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -842,6 +868,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -873,6 +900,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -904,6 +932,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -935,6 +964,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -967,6 +997,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -998,6 +1029,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1027,6 +1059,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1058,6 +1091,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1088,6 +1122,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1117,6 +1152,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1146,6 +1182,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1176,6 +1213,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1208,6 +1246,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1239,6 +1278,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1268,6 +1308,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "50%" }, @@ -1303,6 +1344,7 @@ "reqd": 0, "search_index": 1, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1335,6 +1377,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1365,6 +1408,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1373,7 +1417,7 @@ "bold": 0, "collapsible": 0, "columns": 0, - "fieldname": "subscription", + "fieldname": "auto_repeat", "fieldtype": "Link", "hidden": 0, "ignore_user_permissions": 0, @@ -1382,10 +1426,10 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Subscription", + "label": "Auto Repeat", "length": 0, "no_copy": 1, - "options": "Subscription", + "options": "Auto Repeat", "permlevel": 0, "precision": "", "print_hide": 1, @@ -1396,6 +1440,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1428,6 +1473,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 } ], @@ -1443,7 +1489,7 @@ "istable": 0, "max_attachments": 0, "menu_index": 0, - "modified": "2017-08-31 11:21:09.442695", + "modified": "2018-03-10 07:30:24.319356", "modified_by": "Administrator", "module": "Accounts", "name": "Journal Entry", diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json index 60a54c22ba..564a93c7c7 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.json +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json @@ -1749,7 +1749,7 @@ "bold": 0, "collapsible": 0, "columns": 0, - "fieldname": "subscription", + "fieldname": "auto_repeat", "fieldtype": "Link", "hidden": 0, "ignore_user_permissions": 0, @@ -1758,10 +1758,10 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Subscription", + "label": "Auto Repeat", "length": 0, "no_copy": 1, - "options": "Subscription", + "options": "Auto Repeat", "permlevel": 0, "precision": "", "print_hide": 1, @@ -1848,7 +1848,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-02-19 16:58:23.899015", + "modified": "2018-03-10 07:31:49.264576", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry", diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 7bdb6fbd12..617c670275 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -519,7 +519,7 @@ class PaymentEntry(AccountsController): doc = frappe.get_doc("Expense Claim", d.reference_name) update_reimbursed_amount(doc) - def on_recurring(self, reference_doc, subscription_doc): + def on_recurring(self, reference_doc, auto_repeat_doc): self.reference_no = reference_doc.name self.reference_date = nowdate() diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index d707436077..f6faf110cd 100755 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -41,6 +41,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -73,6 +74,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 1, + "translatable": 0, "unique": 0 }, { @@ -105,6 +107,7 @@ "reqd": 0, "search_index": 1, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -138,6 +141,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -169,6 +173,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -200,6 +205,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -230,6 +236,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -259,6 +266,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "50%" }, @@ -290,6 +298,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -322,6 +331,7 @@ "reqd": 1, "search_index": 1, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -353,6 +363,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "100px" }, @@ -385,6 +396,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -417,6 +429,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -448,6 +461,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -480,6 +494,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -509,6 +524,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -540,6 +556,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -571,6 +588,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -602,6 +620,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -634,6 +653,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -664,6 +684,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -694,6 +715,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -724,6 +746,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -754,6 +777,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -783,6 +807,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -812,6 +837,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -841,6 +867,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -870,6 +897,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -902,6 +930,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -932,6 +961,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -962,6 +992,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -994,6 +1025,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1027,6 +1059,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1055,6 +1088,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1085,6 +1119,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1115,6 +1150,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1145,6 +1181,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1174,6 +1211,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1205,6 +1243,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1236,6 +1275,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1268,6 +1308,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1296,6 +1337,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1327,6 +1369,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1360,6 +1403,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1388,6 +1432,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1419,6 +1464,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1451,6 +1497,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1481,6 +1528,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1512,6 +1560,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1544,6 +1593,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1573,6 +1623,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1604,6 +1655,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1633,6 +1685,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1665,6 +1718,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1695,6 +1749,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1725,6 +1780,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1756,6 +1812,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1788,6 +1845,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1820,6 +1878,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1852,6 +1911,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1881,6 +1941,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1913,6 +1974,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1945,6 +2007,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1976,6 +2039,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2007,6 +2071,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2039,6 +2104,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2070,6 +2136,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2099,6 +2166,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2129,6 +2197,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2160,6 +2229,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2189,6 +2259,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2221,6 +2292,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2252,6 +2324,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2284,6 +2357,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2316,6 +2390,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2345,6 +2420,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "50%" }, @@ -2378,6 +2454,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2409,6 +2486,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2441,6 +2519,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2472,6 +2551,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2504,6 +2584,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2536,6 +2617,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2567,6 +2649,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2599,6 +2682,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2630,6 +2714,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2661,6 +2746,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2690,6 +2776,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2722,6 +2809,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2753,6 +2841,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2785,6 +2874,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2815,6 +2905,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2846,6 +2937,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2875,6 +2967,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2906,6 +2999,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2937,6 +3031,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2969,6 +3064,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3000,6 +3096,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3032,6 +3129,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3063,6 +3161,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3094,6 +3193,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3125,6 +3225,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3156,6 +3257,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3186,6 +3288,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3215,6 +3318,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3245,6 +3349,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3277,6 +3382,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3309,6 +3415,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "50px" }, @@ -3341,6 +3448,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3371,6 +3479,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3402,6 +3511,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3434,6 +3544,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3464,6 +3575,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3495,6 +3607,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3528,6 +3641,7 @@ "reqd": 1, "search_index": 1, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3559,6 +3673,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3593,6 +3708,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3624,6 +3740,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3652,6 +3769,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3684,6 +3802,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3715,6 +3834,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3747,6 +3867,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3777,6 +3898,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3808,6 +3930,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3839,6 +3962,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3868,6 +3992,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3876,7 +4001,7 @@ "bold": 0, "collapsible": 0, "columns": 0, - "fieldname": "subscription", + "fieldname": "auto_repeat", "fieldtype": "Link", "hidden": 0, "ignore_user_permissions": 0, @@ -3885,10 +4010,10 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Subscription", + "label": "Auto Repeat", "length": 0, "no_copy": 1, - "options": "Subscription", + "options": "Auto Repeat", "permlevel": 0, "precision": "", "print_hide": 1, @@ -3899,6 +4024,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 } ], @@ -3914,7 +4040,7 @@ "istable": 0, "max_attachments": 0, "menu_index": 0, - "modified": "2018-01-11 14:44:22.982512", + "modified": "2018-03-10 07:33:12.760250", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index d7e14e1f6c..569664d50b 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -694,7 +694,7 @@ class PurchaseInvoice(BuyingController): if account_type != 'Fixed Asset': frappe.throw(_("Row {0}# Account must be of type 'Fixed Asset'").format(d.idx)) - def on_recurring(self, reference_doc, subscription_doc): + def on_recurring(self, reference_doc, auto_repeat_doc): self.due_date = None @frappe.whitelist() diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 27ecbf7bb8..4eb8c45197 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -15,1263 +15,1304 @@ "engine": "InnoDB", "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "customer_section", - "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": "", - "length": 0, - "no_copy": 0, - "options": "fa fa-user", - "permlevel": 0, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "customer_section", + "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": "", + "length": 0, + "no_copy": 0, + "options": "fa fa-user", + "permlevel": 0, + "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": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "{customer_name}", - "fieldname": "title", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Title", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 1, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "{customer_name}", + "fieldname": "title", + "fieldtype": "Data", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Title", + "length": 0, + "no_copy": 1, + "permlevel": 0, + "precision": "", + "print_hide": 1, + "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": 1, - "collapsible": 0, - "columns": 0, - "fieldname": "naming_series", - "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": "Series", - "length": 0, - "no_copy": 1, - "oldfieldname": "naming_series", - "oldfieldtype": "Select", - "options": "SINV-\nSINV-RET-", - "permlevel": 0, - "print_hide": 1, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 1, + "collapsible": 0, + "columns": 0, + "fieldname": "naming_series", + "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": "Series", + "length": 0, + "no_copy": 1, + "oldfieldname": "naming_series", + "oldfieldtype": "Select", + "options": "SINV-\nSINV-RET-", + "permlevel": 0, + "print_hide": 1, + "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": 1, - "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": 0, - "in_standard_filter": 1, - "label": "Customer", - "length": 0, - "no_copy": 0, - "oldfieldname": "customer", - "oldfieldtype": "Link", - "options": "Customer", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 1, - "set_only_once": 0, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 1, + "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": 0, + "in_standard_filter": 1, + "label": "Customer", + "length": 0, + "no_copy": 0, + "oldfieldname": "customer", + "oldfieldtype": "Link", + "options": "Customer", + "permlevel": 0, + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 1, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 1, - "collapsible": 0, - "columns": 0, - "depends_on": "customer", - "fieldname": "customer_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Customer Name", - "length": 0, - "no_copy": 0, - "oldfieldname": "customer_name", - "oldfieldtype": "Data", - "options": "customer.customer_name", - "permlevel": 0, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 1, + "collapsible": 0, + "columns": 0, + "depends_on": "customer", + "fieldname": "customer_name", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 1, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Customer Name", + "length": 0, + "no_copy": 0, + "oldfieldname": "customer_name", + "oldfieldtype": "Data", + "options": "customer.customer_name", + "permlevel": 0, + "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": "tax_id", - "fieldtype": "Data", - "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": "Tax Id", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "tax_id", + "fieldtype": "Data", + "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": "Tax Id", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 1, + "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": "project", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Project", - "length": 0, - "no_copy": 0, - "oldfieldname": "project_name", - "oldfieldtype": "Link", - "options": "Project", - "permlevel": 0, - "print_hide": 1, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "project", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 1, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Project", + "length": 0, + "no_copy": 0, + "oldfieldname": "project_name", + "oldfieldtype": "Link", + "options": "Project", + "permlevel": 0, + "print_hide": 1, + "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": "is_pos", - "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": "Include Payment (POS)", - "length": 0, - "no_copy": 0, - "oldfieldname": "is_pos", - "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 1, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "is_pos", + "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": "Include Payment (POS)", + "length": 0, + "no_copy": 0, + "oldfieldname": "is_pos", + "oldfieldtype": "Check", + "permlevel": 0, + "print_hide": 1, + "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, - "depends_on": "is_pos", - "fieldname": "pos_profile", - "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": "POS Profile", - "length": 0, - "no_copy": 0, - "options": "POS Profile", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "is_pos", + "fieldname": "pos_profile", + "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": "POS Profile", + "length": 0, + "no_copy": 0, + "options": "POS Profile", + "permlevel": 0, + "precision": "", + "print_hide": 1, + "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": "offline_pos_name", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Offline POS Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "offline_pos_name", + "fieldtype": "Data", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Offline POS Name", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 1, + "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": "column_break1", - "fieldtype": "Column 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, - "length": 0, - "no_copy": 0, - "oldfieldtype": "Column Break", - "permlevel": 0, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break1", + "fieldtype": "Column 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, + "length": 0, + "no_copy": 0, + "oldfieldtype": "Column Break", + "permlevel": 0, + "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": "company", - "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": 1, - "label": "Company", - "length": 0, - "no_copy": 0, - "oldfieldname": "company", - "oldfieldtype": "Link", - "options": "Company", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 1, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "company", + "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": 1, + "label": "Company", + "length": 0, + "no_copy": 0, + "oldfieldname": "company", + "oldfieldtype": "Link", + "options": "Company", + "permlevel": 0, + "print_hide": 1, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 1, + "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": 1, - "collapsible": 0, - "columns": 0, - "default": "Today", - "fieldname": "posting_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": "Date", - "length": 0, - "no_copy": 1, - "oldfieldname": "posting_date", - "oldfieldtype": "Date", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 1, - "set_only_once": 0, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 1, + "collapsible": 0, + "columns": 0, + "default": "Today", + "fieldname": "posting_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": "Date", + "length": 0, + "no_copy": 1, + "oldfieldname": "posting_date", + "oldfieldtype": "Date", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 1, + "set_only_once": 0, + "translatable": 0, "unique": 0 - }, + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "posting_time", - "fieldtype": "Time", - "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": "Posting Time", - "length": 0, - "no_copy": 1, - "oldfieldname": "posting_time", - "oldfieldtype": "Time", - "permlevel": 0, - "print_hide": 1, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "posting_time", + "fieldtype": "Time", + "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": "Posting Time", + "length": 0, + "no_copy": 1, + "oldfieldname": "posting_time", + "oldfieldtype": "Time", + "permlevel": 0, + "print_hide": 1, + "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, - "depends_on": "eval:doc.docstatus==0", - "fieldname": "set_posting_time", - "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": "Edit Posting Date and Time", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "eval:doc.docstatus==0", + "fieldname": "set_posting_time", + "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": "Edit Posting Date and Time", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 1, + "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": "due_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": "Payment Due Date", - "length": 0, - "no_copy": 1, - "oldfieldname": "due_date", - "oldfieldtype": "Date", - "permlevel": 0, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "due_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": "Payment Due Date", + "length": 0, + "no_copy": 1, + "oldfieldname": "due_date", + "oldfieldtype": "Date", + "permlevel": 0, + "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": "amended_from", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 1, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Amended From", - "length": 0, - "no_copy": 1, - "oldfieldname": "amended_from", - "oldfieldtype": "Link", - "options": "Sales Invoice", - "permlevel": 0, - "print_hide": 1, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "amended_from", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 1, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Amended From", + "length": 0, + "no_copy": 1, + "oldfieldname": "amended_from", + "oldfieldtype": "Link", + "options": "Sales Invoice", + "permlevel": 0, + "print_hide": 1, + "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": "is_return", - "fieldname": "returns", - "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": "Returns", - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "is_return", + "fieldname": "returns", + "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": "Returns", + "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, - "depends_on": "is_return", - "fieldname": "is_return", - "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": "Is Return", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "is_return", + "fieldname": "is_return", + "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": "Is Return", + "length": 0, + "no_copy": 1, + "permlevel": 0, + "precision": "", + "print_hide": 1, + "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": "is_return", - "fieldname": "return_against", - "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": "Return Against Sales Invoice", - "length": 0, - "no_copy": 1, - "options": "Sales Invoice", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "is_return", + "fieldname": "return_against", + "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": "Return Against Sales Invoice", + "length": 0, + "no_copy": 1, + "options": "Sales Invoice", + "permlevel": 0, + "precision": "", + "print_hide": 1, + "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": 1, - "collapsible_depends_on": "po_no", - "columns": 0, - "fieldname": "customer_po_details", - "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": "Customer PO Details", - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "collapsible_depends_on": "po_no", + "columns": 0, + "fieldname": "customer_po_details", + "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": "Customer PO Details", + "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": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "po_no", - "fieldtype": "Data", - "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": "Customer's Purchase Order", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 1, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "po_no", + "fieldtype": "Data", + "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": "Customer's Purchase Order", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 1, + "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": "column_break_23", - "fieldtype": "Column 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, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_23", + "fieldtype": "Column 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, + "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": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "po_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": "Customer's Purchase Order 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, + "allow_bulk_edit": 0, + "allow_on_submit": 1, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "po_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": "Customer's Purchase Order 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": 1, - "columns": 0, - "depends_on": "", - "fieldname": "address_and_contact", - "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": "Address and Contact", - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "columns": 0, + "depends_on": "", + "fieldname": "address_and_contact", + "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": "Address and Contact", + "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": "customer_address", - "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": "Customer Address", - "length": 0, - "no_copy": 0, - "options": "Address", - "permlevel": 0, - "print_hide": 1, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "customer_address", + "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": "Customer Address", + "length": 0, + "no_copy": 0, + "options": "Address", + "permlevel": 0, + "print_hide": 1, + "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": "address_display", - "fieldtype": "Small Text", - "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": "Address", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "address_display", + "fieldtype": "Small Text", + "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": "Address", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "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": "contact_person", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Contact Person", - "length": 0, - "no_copy": 0, - "options": "Contact", - "permlevel": 0, - "print_hide": 1, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "contact_person", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 1, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Contact Person", + "length": 0, + "no_copy": 0, + "options": "Contact", + "permlevel": 0, + "print_hide": 1, + "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": "contact_display", - "fieldtype": "Small Text", - "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": "Contact", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "contact_display", + "fieldtype": "Small Text", + "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": "Contact", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "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": "contact_mobile", - "fieldtype": "Small Text", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Mobile No", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "contact_mobile", + "fieldtype": "Small Text", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Mobile No", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "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": "contact_email", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Contact Email", - "length": 0, - "no_copy": 0, - "options": "Email", - "permlevel": 0, - "print_hide": 1, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "contact_email", + "fieldtype": "Data", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Contact Email", + "length": 0, + "no_copy": 0, + "options": "Email", + "permlevel": 0, + "print_hide": 1, + "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": "territory", - "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": "Territory", - "length": 0, - "no_copy": 0, - "options": "Territory", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "territory", + "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": "Territory", + "length": 0, + "no_copy": 0, + "options": "Territory", + "permlevel": 0, + "precision": "", + "print_hide": 1, + "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": "col_break4", - "fieldtype": "Column 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, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "col_break4", + "fieldtype": "Column 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, + "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": "shipping_address_name", - "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": "Shipping Address Name", - "length": 0, - "no_copy": 0, - "options": "Address", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "shipping_address_name", + "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": "Shipping Address Name", + "length": 0, + "no_copy": 0, + "options": "Address", + "permlevel": 0, + "precision": "", + "print_hide": 1, + "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": "shipping_address", - "fieldtype": "Small Text", - "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": "Shipping Address", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "shipping_address", + "fieldtype": "Small Text", + "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": "Shipping Address", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 1, + "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": "company_address", - "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": "Company Address Name", - "length": 0, - "no_copy": 0, - "options": "Address", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "company_address", + "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": "Company Address Name", + "length": 0, + "no_copy": 0, + "options": "Address", + "permlevel": 0, + "precision": "", + "print_hide": 1, + "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": "company_address_display", - "fieldtype": "Small Text", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Company Address", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "company_address_display", + "fieldtype": "Small Text", + "hidden": 1, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Company Address", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 1, + "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": 1, - "columns": 0, - "depends_on": "customer", - "fieldname": "currency_and_price_list", - "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": "Currency and Price List", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 1, + "columns": 0, + "depends_on": "customer", + "fieldname": "currency_and_price_list", + "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": "Currency and Price List", + "length": 0, + "no_copy": 0, + "options": "", + "permlevel": 0, + "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": "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, - "oldfieldname": "currency", - "oldfieldtype": "Select", - "options": "Currency", - "permlevel": 0, - "print_hide": 1, - "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, + "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, + "oldfieldname": "currency", + "oldfieldtype": "Select", + "options": "Currency", + "permlevel": 0, + "print_hide": 1, + "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, - "description": "Rate at which Customer Currency is converted to customer's base currency", - "fieldname": "conversion_rate", - "fieldtype": "Float", - "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": "Exchange Rate", - "length": 0, - "no_copy": 0, - "oldfieldname": "conversion_rate", - "oldfieldtype": "Currency", - "permlevel": 0, - "precision": "9", - "print_hide": 1, - "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, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "Rate at which Customer Currency is converted to customer's base currency", + "fieldname": "conversion_rate", + "fieldtype": "Float", + "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": "Exchange Rate", + "length": 0, + "no_copy": 0, + "oldfieldname": "conversion_rate", + "oldfieldtype": "Currency", + "permlevel": 0, + "precision": "9", + "print_hide": 1, + "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": "column_break2", - "fieldtype": "Column 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, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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, - "unique": 0, + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break2", + "fieldtype": "Column 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, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "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, "width": "50%" }, { @@ -1304,6 +1345,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1334,6 +1376,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1365,6 +1408,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1394,6 +1438,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1425,6 +1470,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1456,6 +1502,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1488,6 +1535,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1518,6 +1566,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1548,6 +1597,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1577,6 +1627,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1609,6 +1660,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1640,6 +1692,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1671,6 +1724,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1699,6 +1753,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1730,6 +1785,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1762,6 +1818,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1790,6 +1847,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1821,6 +1879,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1851,6 +1910,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1881,6 +1941,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1912,6 +1973,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1944,6 +2006,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1972,6 +2035,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2003,6 +2067,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2031,6 +2096,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2063,6 +2129,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2093,6 +2160,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2123,6 +2191,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2151,6 +2220,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2183,6 +2253,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2212,6 +2283,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2242,6 +2314,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2273,6 +2346,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2305,6 +2379,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2336,6 +2411,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2364,6 +2440,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2394,6 +2471,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2424,6 +2502,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2455,6 +2534,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2487,6 +2567,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2518,6 +2599,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2550,6 +2632,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2582,6 +2665,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2611,6 +2695,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "50%" }, @@ -2644,6 +2729,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2675,6 +2761,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2707,6 +2794,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2738,6 +2826,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2770,6 +2859,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2802,6 +2892,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2834,6 +2925,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2865,6 +2957,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2897,6 +2990,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2928,6 +3022,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2960,6 +3055,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2992,6 +3088,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3024,6 +3121,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3057,6 +3155,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3089,6 +3188,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3118,6 +3218,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3149,6 +3250,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3178,6 +3280,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3211,6 +3314,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3240,6 +3344,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3272,6 +3377,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3301,6 +3407,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3333,6 +3440,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3365,6 +3473,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3396,6 +3505,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "50%" }, @@ -3428,6 +3538,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3459,6 +3570,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3489,6 +3601,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3519,6 +3632,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3550,6 +3664,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3581,6 +3696,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3613,6 +3729,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3645,6 +3762,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3676,6 +3794,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3706,6 +3825,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3738,6 +3858,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3768,6 +3889,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3797,6 +3919,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3829,6 +3952,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3860,6 +3984,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3891,6 +4016,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3924,6 +4050,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3952,6 +4079,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "50%" }, @@ -3985,6 +4113,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -4017,6 +4146,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -4048,6 +4178,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -4081,6 +4212,7 @@ "reqd": 1, "search_index": 1, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -4112,6 +4244,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -4146,6 +4279,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -4176,6 +4310,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -4206,6 +4341,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -4235,6 +4371,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -4266,6 +4403,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -4298,6 +4436,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -4330,6 +4469,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -4359,6 +4499,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "50%" }, @@ -4391,6 +4532,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -4423,6 +4565,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -4453,6 +4596,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -4485,6 +4629,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -4515,6 +4660,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -4546,6 +4692,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -4577,6 +4724,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -4606,6 +4754,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -4614,7 +4763,7 @@ "bold": 0, "collapsible": 0, "columns": 0, - "fieldname": "subscription", + "fieldname": "auto_repeat", "fieldtype": "Link", "hidden": 0, "ignore_user_permissions": 0, @@ -4623,10 +4772,10 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Subscription", + "label": "Auto Repeat", "length": 0, "no_copy": 1, - "options": "Subscription", + "options": "Auto Repeat", "permlevel": 0, "precision": "", "print_hide": 1, @@ -4637,6 +4786,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -4668,6 +4818,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -4698,6 +4849,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 } ], @@ -4713,7 +4865,7 @@ "istable": 0, "max_attachments": 0, "menu_index": 0, - "modified": "2018-02-08 05:16:44.331093", + "modified": "2018-03-10 07:31:12.151213", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", @@ -4812,4 +4964,4 @@ "title_field": "title", "track_changes": 1, "track_seen": 1 -} +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 44658f7195..cdefd40c3c 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -110,7 +110,7 @@ class SalesInvoice(SellingController): def on_submit(self): self.validate_pos_paid_amount() - if not self.subscription: + if not self.auto_repeat: frappe.get_doc('Authorization Control').validate_approving_authority(self.doctype, self.company, self.base_grand_total, self) @@ -828,7 +828,7 @@ class SalesInvoice(SellingController): for dn in set(updated_delivery_notes): frappe.get_doc("Delivery Note", dn).update_billing_percentage(update_modified=update_modified) - def on_recurring(self, reference_doc, subscription_doc): + def on_recurring(self, reference_doc, auto_repeat_doc): for fieldname in ("c_form_applicable", "c_form_no", "write_off_amount"): self.set(fieldname, reference_doc.get(fieldname)) diff --git a/erpnext/accounts/doctype/subscription/subscription.js b/erpnext/accounts/doctype/subscription/subscription.js index 8db5be8772..8d8f2cc9b4 100644 --- a/erpnext/accounts/doctype/subscription/subscription.js +++ b/erpnext/accounts/doctype/subscription/subscription.js @@ -1,75 +1,78 @@ -// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors +// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt frappe.ui.form.on('Subscription', { - setup: function(frm) { - frm.fields_dict['reference_doctype'].get_query = function(doc) { - return { - query: "erpnext.accounts.doctype.subscription.subscription.subscription_doctype_query" - }; - }; - - frm.fields_dict['reference_document'].get_query = function() { - return { - filters: { - "docstatus": 1, - "subscription": '' - } - }; - }; - - frm.fields_dict['print_format'].get_query = function() { - return { - filters: { - "doc_type": frm.doc.reference_doctype - } - }; - }; - }, - refresh: function(frm) { - if(frm.doc.docstatus == 1) { - let label = __('View {0}', [frm.doc.reference_doctype]); - frm.add_custom_button(__(label), - function() { - frappe.route_options = { - "subscription": frm.doc.name, - }; - frappe.set_route("List", frm.doc.reference_doctype); - } - ); - - if(frm.doc.status != 'Stopped') { - frm.add_custom_button(__("Stop"), - function() { - frm.events.stop_resume_subscription(frm, "Stopped"); - } + 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) ); } - - if(frm.doc.status == 'Stopped') { - frm.add_custom_button(__("Resume"), - function() { - frm.events.stop_resume_subscription(frm, "Resumed"); - } + else if(frm.doc.status === 'Canceled'){ + frm.add_custom_button( + __('Restart Subscription'), + () => frm.events.renew_this_subscription(frm) ); } } }, - stop_resume_subscription: function(frm, status) { + 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.subscription.subscription.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.subscription.subscription.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.subscription.subscription.stop_resume_subscription", - args: { - subscription: frm.doc.name, - status: status - }, - callback: function(r) { - if(r.message) { - frm.set_value("status", r.message); + method: + "erpnext.accounts.doctype.subscription.subscription.get_subscription_updates", + args: {name: doc.name}, + freeze: true, + callback: function(data){ + if(!data.exc){ frm.reload_doc(); } } }); } -}); \ No newline at end of file +}); diff --git a/erpnext/accounts/doctype/subscription/subscription.json b/erpnext/accounts/doctype/subscription/subscription.json index 7ff2e4b632..80cbd85e70 100644 --- a/erpnext/accounts/doctype/subscription/subscription.json +++ b/erpnext/accounts/doctype/subscription/subscription.json @@ -1,9 +1,9 @@ { "allow_copy": 0, "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "naming_series:", + "allow_import": 0, + "allow_rename": 0, + "autoname": "SUBC.####", "beta": 0, "creation": "2017-07-18 17:50:43.967266", "custom": 0, @@ -19,67 +19,7 @@ "bold": 0, "collapsible": 0, "columns": 0, - "fieldname": "section_break_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, - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "naming_series", - "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": "Series", - "length": 0, - "no_copy": 0, - "options": "SUB-", - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "reference_doctype", + "fieldname": "subscriber", "fieldtype": "Link", "hidden": 0, "ignore_user_permissions": 0, @@ -88,10 +28,10 @@ "in_global_search": 0, "in_list_view": 1, "in_standard_filter": 0, - "label": "Reference Doctype", + "label": "Subscriber", "length": 0, "no_copy": 0, - "options": "DocType", + "options": "Subscriber", "permlevel": 0, "precision": "", "print_hide": 0, @@ -101,7 +41,8 @@ "report_hide": 0, "reqd": 1, "search_index": 0, - "set_only_once": 0, + "set_only_once": 1, + "translatable": 0, "unique": 0 }, { @@ -110,127 +51,7 @@ "bold": 0, "collapsible": 0, "columns": 0, - "fieldname": "reference_document", - "fieldtype": "Dynamic 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": "Reference Document", - "length": 0, - "no_copy": 1, - "options": "reference_doctype", - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_5", - "fieldtype": "Column 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, - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "start_date", - "fieldtype": "Date", - "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": "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": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "end_date", - "fieldtype": "Date", - "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": "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": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "submit_on_creation", + "fieldname": "cancel_at_period_end", "fieldtype": "Check", "hidden": 0, "ignore_user_permissions": 0, @@ -239,9 +60,10 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Submit on Creation", + "label": "Cancel At End Of Period", "length": 0, "no_copy": 0, + "options": "", "permlevel": 0, "precision": "", "print_hide": 0, @@ -252,36 +74,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "disabled", - "fieldtype": "Check", - "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": "Disabled", - "length": 0, - "no_copy": 1, - "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 }, { @@ -290,127 +83,7 @@ "bold": 0, "collapsible": 0, "columns": 0, - "fieldname": "section_break_10", - "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, - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "frequency", - "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": "Frequency", - "length": 0, - "no_copy": 0, - "options": "\nDaily\nWeekly\nMonthly\nQuarterly\nHalf-yearly\nYearly", - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_13", - "fieldtype": "Column 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, - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval: in_list([\"Monthly\", \"Quarterly\", \"Yearly\"], doc.frequency)", - "fieldname": "repeat_on_day", - "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": "Repeat on Day", - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "next_schedule_date", + "fieldname": "start", "fieldtype": "Date", "hidden": 0, "ignore_user_permissions": 0, @@ -419,12 +92,45 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Next Schedule Date", + "label": "Subscription Start Date", "length": 0, - "no_copy": 1, + "no_copy": 0, + "options": "", "permlevel": 0, "precision": "", - "print_hide": 1, + "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, + "options": "", + "permlevel": 0, + "precision": "", + "print_hide": 0, "print_hide_if_no_value": 0, "read_only": 1, "remember_last_selected_value": 0, @@ -432,16 +138,17 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, "allow_on_submit": 0, "bold": 0, - "collapsible": 1, + "collapsible": 0, "columns": 0, - "fieldname": "notification", - "fieldtype": "Section Break", + "fieldname": "current_invoice_start", + "fieldtype": "Date", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -449,7 +156,69 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Notification", + "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": 1, + "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, @@ -461,7 +230,8 @@ "report_hide": 0, "reqd": 0, "search_index": 0, - "set_only_once": 0, + "set_only_once": 1, + "translatable": 0, "unique": 0 }, { @@ -470,8 +240,9 @@ "bold": 0, "collapsible": 0, "columns": 0, - "fieldname": "notify_by_email", - "fieldtype": "Check", + "depends_on": "eval:doc.trial_period_start", + "fieldname": "trial_period_end", + "fieldtype": "Date", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -479,7 +250,7 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Notify by Email", + "label": "Trial Period End Date", "length": 0, "no_copy": 0, "permlevel": 0, @@ -491,48 +262,17 @@ "report_hide": 0, "reqd": 0, "search_index": 0, - "set_only_once": 0, + "set_only_once": 1, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, - "allow_on_submit": 0, + "allow_on_submit": 1, "bold": 0, "collapsible": 0, "columns": 0, - "depends_on": "eval: doc.notify_by_email", - "description": "To add dynamic subject, use jinja tags like\n\n

New {{ doc.doctype }} #{{ doc.name }}
", - "fieldname": "subject", - "fieldtype": "Data", - "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": "Subject", - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_17", + "fieldname": "cb_1", "fieldtype": "Column Break", "hidden": 0, "ignore_user_permissions": 0, @@ -541,161 +281,6 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "notify_by_email", - "fieldname": "recipients", - "fieldtype": "Small Text", - "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": "Recipients", - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "notify_by_email", - "fieldname": "print_format", - "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": "Print Format", - "length": 0, - "no_copy": 0, - "options": "Print Format", - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "depends_on": "eval:doc.notify_by_email", - "fieldname": "section_break_20", - "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": "Message", - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Please find attached {{ doc.doctype }} #{{ doc.name }}", - "fieldname": "message", - "fieldtype": "Text", - "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": "Message", - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "depends_on": "eval: !doc.__islocal", - "fieldname": "section_break_16", - "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": "", "length": 0, "no_copy": 0, @@ -709,15 +294,15 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { "allow_bulk_edit": 0, - "allow_on_submit": 1, + "allow_on_submit": 0, "bold": 0, "collapsible": 0, "columns": 0, - "default": "Draft", "fieldname": "status", "fieldtype": "Select", "hidden": 0, @@ -725,12 +310,12 @@ "ignore_xss_filter": 0, "in_filter": 0, "in_global_search": 0, - "in_list_view": 1, + "in_list_view": 0, "in_standard_filter": 0, "label": "Status", "length": 0, "no_copy": 0, - "options": "\nDraft\nStopped\nSubmitted\nCancelled\nCompleted", + "options": "\nTrialling\nActive\nPast Due Date\nCanceled\nUnpaid", "permlevel": 0, "precision": "", "print_hide": 0, @@ -741,6 +326,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -749,7 +335,168 @@ "bold": 0, "collapsible": 0, "columns": 0, - "fieldname": "amended_from", + "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, + "options": "", + "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": 1, + "bold": 0, + "collapsible": 0, + "columns": 0, + "depends_on": "", + "fieldname": "sb_4", + "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": "Plans", + "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": 1, + "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, @@ -758,19 +505,249 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Amended From", + "label": "Sales Taxes and Charges Template", "length": 0, - "no_copy": 1, - "options": "Subscription", + "no_copy": 0, + "options": "Sales Taxes and Charges Template", "permlevel": 0, - "print_hide": 1, + "precision": "", + "print_hide": 0, "print_hide_if_no_value": 0, - "read_only": 1, + "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, + "depends_on": "", + "description": "", + "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, + "depends_on": "", + "fieldname": "cb_2", + "fieldtype": "Column 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": "", + "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, + "depends_on": "", + "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, + "options": "", + "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": 1, + "columns": 0, + "depends_on": "", + "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, + "default": "", + "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": 1, + "columns": 0, + "depends_on": "", + "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 } ], @@ -780,11 +757,11 @@ "idx": 0, "image_view": 0, "in_create": 0, - "is_submittable": 1, + "is_submittable": 0, "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-10-23 18:28:08.966403", + "modified": "2018-03-12 00:34:52.243133", "modified_by": "Administrator", "module": "Accounts", "name": "Subscription", @@ -794,7 +771,7 @@ { "amend": 0, "apply_user_permissions": 0, - "cancel": 1, + "cancel": 0, "create": 1, "delete": 1, "email": 1, @@ -808,13 +785,13 @@ "role": "System Manager", "set_user_permissions": 0, "share": 1, - "submit": 1, + "submit": 0, "write": 1 }, { "amend": 0, "apply_user_permissions": 0, - "cancel": 1, + "cancel": 0, "create": 1, "delete": 1, "email": 1, @@ -828,13 +805,13 @@ "role": "Accounts User", "set_user_permissions": 0, "share": 1, - "submit": 1, + "submit": 0, "write": 1 }, { "amend": 0, "apply_user_permissions": 0, - "cancel": 1, + "cancel": 0, "create": 1, "delete": 1, "email": 1, @@ -848,18 +825,18 @@ "role": "Accounts Manager", "set_user_permissions": 0, "share": 1, - "submit": 1, + "submit": 0, "write": 1 } ], "quick_entry": 0, "read_only": 0, "read_only_onload": 0, - "search_fields": "reference_document", + "search_fields": "", "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "DESC", - "title_field": "reference_document", + "title_field": "", "track_changes": 1, "track_seen": 0 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py index 480abd42ee..7b013bb7be 100644 --- a/erpnext/accounts/doctype/subscription/subscription.py +++ b/erpnext/accounts/doctype/subscription/subscription.py @@ -1,315 +1,499 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors +# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt from __future__ import unicode_literals + import frappe -import calendar from frappe import _ -from frappe.desk.form import assign_to -from frappe.utils.jinja import validate_template -from dateutil.relativedelta import relativedelta -from frappe.utils.user import get_system_managers -from frappe.utils import cstr, getdate, split_emails, add_days, today, get_last_day, get_first_day from frappe.model.document import Document +from frappe.utils.data import nowdate, getdate, cint, add_days, date_diff, get_last_day, add_to_date, flt + -month_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, 'Yearly': 12} class Subscription(Document): - def validate(self): - self.update_status() - self.validate_reference_doctype() - self.validate_dates() - self.validate_next_schedule_date() - self.validate_email_id() + def before_insert(self): + # update start just before the subscription doc is created + self.update_subscription_period(self.start) - validate_template(self.subject or "") - validate_template(self.message or "") + 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. - def before_submit(self): - if not self.next_schedule_date: - self.next_schedule_date = get_next_schedule_date(self.start_date, - self.frequency, self.repeat_on_day) + 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 on_submit(self): - self.update_subscription_id() + 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 on_update_after_submit(self): - self.validate_dates() - self.set_next_schedule_date() + def set_current_invoice_end(self): + """ + This sets the date of the end of the current billing period. - def before_cancel(self): - self.unlink_subscription_id() - self.next_schedule_date = None + If the subscription is in trial period, it will be set as the end of the + trial period. - def unlink_subscription_id(self): - frappe.db.sql("update `tab{0}` set subscription = null where subscription=%s" - .format(self.reference_doctype), self.name) - - def validate_reference_doctype(self): - if not frappe.get_meta(self.reference_doctype).has_field('subscription'): - frappe.throw(_("Add custom field Subscription in the doctype {0}").format(self.reference_doctype)) - - def validate_dates(self): - if self.end_date and getdate(self.start_date) > getdate(self.end_date): - frappe.throw(_("End date must be greater than start date")) - - def validate_next_schedule_date(self): - if self.repeat_on_day and self.next_schedule_date: - next_date = getdate(self.next_schedule_date) - if next_date.day != self.repeat_on_day: - # if the repeat day is the last day of the month (31) - # and the current month does not have as many days, - # then the last day of the current month is a valid date - lastday = calendar.monthrange(next_date.year, next_date.month)[1] - if self.repeat_on_day < lastday: - - # the specified day of the month is not same as the day specified - # or the last day of the month - frappe.throw(_("Next Date's day and Repeat on Day of Month must be equal")) - - def validate_email_id(self): - if self.notify_by_email: - if self.recipients: - email_list = split_emails(self.recipients.replace("\n", "")) - - from frappe.utils import validate_email_add - for email in email_list: - if not validate_email_add(email): - frappe.throw(_("{0} is an invalid email address in 'Recipients'").format(email)) - else: - frappe.throw(_("'Recipients' not specified")) - - def set_next_schedule_date(self): - if self.repeat_on_day: - self.next_schedule_date = get_next_date(self.next_schedule_date, 0, self.repeat_on_day) - - def update_subscription_id(self): - frappe.db.set_value(self.reference_doctype, self.reference_document, "subscription", self.name) - - def update_status(self, status=None): - self.status = { - '0': 'Draft', - '1': 'Submitted', - '2': 'Cancelled' - }[cstr(self.docstatus or 0)] - - if status and status != 'Resumed': - self.status = status - -def get_next_schedule_date(start_date, frequency, repeat_on_day): - mcount = month_map.get(frequency) - if mcount: - next_date = get_next_date(start_date, mcount, repeat_on_day) - else: - days = 7 if frequency == 'Weekly' else 1 - next_date = add_days(start_date, days) - return next_date - -def make_subscription_entry(date=None): - date = date or today() - for data in get_subscription_entries(date): - schedule_date = getdate(data.next_schedule_date) - while schedule_date <= getdate(today()): - create_documents(data, schedule_date) - schedule_date = get_next_schedule_date(schedule_date, - data.frequency, data.repeat_on_day) - - if schedule_date and not frappe.db.get_value('Subscription', data.name, 'disabled'): - frappe.db.set_value('Subscription', data.name, 'next_schedule_date', schedule_date) - -def get_subscription_entries(date): - return frappe.db.sql(""" select * from `tabSubscription` - where docstatus = 1 and next_schedule_date <=%s - and reference_document is not null and reference_document != '' - and next_schedule_date <= ifnull(end_date, '2199-12-31') - and ifnull(disabled, 0) = 0 and status != 'Stopped' """, (date), as_dict=1) - -def create_documents(data, schedule_date): - try: - doc = make_new_document(data, schedule_date) - if data.notify_by_email and data.recipients: - print_format = data.print_format or "Standard" - send_notification(doc, data, print_format=print_format) - - frappe.db.commit() - except Exception: - frappe.db.rollback() - frappe.db.begin() - frappe.log_error(frappe.get_traceback()) - disable_subscription(data) - frappe.db.commit() - if data.reference_document and not frappe.flags.in_test: - notify_error_to_user(data) - -def disable_subscription(data): - subscription = frappe.get_doc('Subscription', data.name) - subscription.db_set('disabled', 1) - -def notify_error_to_user(data): - party = '' - party_type = '' - - if data.reference_doctype in ['Sales Order', 'Sales Invoice', 'Delivery Note']: - party_type = 'customer' - elif data.reference_doctype in ['Purchase Order', 'Purchase Invoice', 'Purchase Receipt']: - party_type = 'supplier' - - if party_type: - party = frappe.db.get_value(data.reference_doctype, data.reference_document, party_type) - - notify_errors(data.reference_document, data.reference_doctype, party, data.owner, data.name) - -def make_new_document(args, schedule_date): - doc = frappe.get_doc(args.reference_doctype, args.reference_document) - new_doc = frappe.copy_doc(doc, ignore_no_copy=False) - update_doc(new_doc, doc , args, schedule_date) - new_doc.insert(ignore_permissions=True) - - if args.submit_on_creation: - new_doc.submit() - - return new_doc - -def update_doc(new_document, reference_doc, args, schedule_date): - new_document.docstatus = 0 - if new_document.meta.get_field('set_posting_time'): - new_document.set('set_posting_time', 1) - - mcount = month_map.get(args.frequency) - - if new_document.meta.get_field('subscription'): - new_document.set('subscription', args.name) - - for fieldname in ['naming_series', 'ignore_pricing_rule', 'posting_time' - 'select_print_heading', 'remarks', 'owner']: - if new_document.meta.get_field(fieldname): - new_document.set(fieldname, reference_doc.get(fieldname)) - - # copy item fields - if new_document.meta.get_field('items'): - for i, item in enumerate(new_document.items): - for fieldname in ("page_break",): - item.set(fieldname, reference_doc.items[i].get(fieldname)) - - for data in new_document.meta.fields: - if data.fieldtype == 'Date' and data.reqd: - new_document.set(data.fieldname, schedule_date) - - set_subscription_period(args, mcount, new_document) - - new_document.run_method("on_recurring", reference_doc=reference_doc, subscription_doc=args) - -def set_subscription_period(args, mcount, new_document): - if mcount and new_document.meta.get_field('from_date') and new_document.meta.get_field('to_date'): - last_ref_doc = frappe.db.sql(""" - select name, from_date, to_date - from `tab{0}` - where subscription=%s and docstatus < 2 - order by creation desc - limit 1 - """.format(args.reference_doctype), args.name, as_dict=1) - - if not last_ref_doc: - return - - from_date = get_next_date(last_ref_doc[0].from_date, mcount) - - if (cstr(get_first_day(last_ref_doc[0].from_date)) == cstr(last_ref_doc[0].from_date)) and \ - (cstr(get_last_day(last_ref_doc[0].to_date)) == cstr(last_ref_doc[0].to_date)): - to_date = get_last_day(get_next_date(last_ref_doc[0].to_date, mcount)) + 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: - to_date = get_next_date(last_ref_doc[0].to_date, mcount) + 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) - new_document.set('from_date', from_date) - new_document.set('to_date', to_date) + 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() -def get_next_date(dt, mcount, day=None): - dt = getdate(dt) - dt += relativedelta(months=mcount, day=day) + @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')) - return dt + def get_billing_cycle_and_interval(self): + """ + Returns a dict representing the billing interval and cycle for this `Subscription`. -def send_notification(new_rv, subscription_doc, print_format='Standard'): - """Notify concerned persons about recurring document generation""" - print_format = print_format - subject = subscription_doc.subject or '' - message = subscription_doc.message or '' + 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 + ) - if not subscription_doc.subject: - subject = _("New {0}: #{1}").format(new_rv.doctype, new_rv.name) - elif "{" in subscription_doc.subject: - subject = frappe.render_template(subscription_doc.subject, {'doc': new_rv}) + return billing_info - if not subscription_doc.message: - message = _("Please find attached {0} #{1}").format(new_rv.doctype, new_rv.name) - elif "{" in subscription_doc.message: - message = frappe.render_template(subscription_doc.message, {'doc': new_rv}) + def get_billing_cycle_data(self): + """ + Returns dict contain the billing cycle data. - attachments = [frappe.attach_print(new_rv.doctype, new_rv.name, - file_name=new_rv.name, print_format=print_format)] + You shouldn't need to call this directly. Use `get_billing_cycle` instead. + """ + billing_info = self.get_billing_cycle_and_interval() - frappe.sendmail(subscription_doc.recipients, - subject=subject, message=message, attachments=attachments) + self.validate_plans_billing_cycle(billing_info) -def notify_errors(doc, doctype, party, owner, name): - recipients = get_system_managers(only_name=True) - frappe.sendmail(recipients + [frappe.db.get_value("User", owner, "email")], - subject=_("[Urgent] Error while creating recurring %s for %s" % (doctype, doc)), - message = frappe.get_template("templates/emails/recurring_document_failed.html").render({ - "type": _(doctype), - "name": doc, - "party": party or "", - "subscription": name - })) + 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 - assign_task_to_owner(name, "Recurring Documents Failed", recipients) + 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.discount_amount = self.additional_discount_amount + + if self.additional_discount_percentage or self.additional_discount_amount: + discount_on = self.apply_additional_discount + invoice.apply_additional_discount = discount_on if discount_on else '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. + """ + if self.status != 'Canceled': + 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. + """ + if self.status == 'Canceled': + self.status = 'Active' + self.db_set('start', nowdate()) + self.update_subscription_period(nowdate()) + self.invoices = [] + self.save() + else: + frappe.throw(_('You cannot restart a Subscription that is not cancelled.')) + + def get_precision(self): + invoice = self.get_current_invoice() + if invoice: + return invoice.precision('grand_total') + + +def get_prorata_factor(period_end, period_start): + diff = flt(date_diff(nowdate(), period_start) + 1) + plan_days = flt(date_diff(period_end, period_start) + 1) + prorate_factor = diff / plan_days + + return prorate_factor + + +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 `tabSubscription` 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('Subscription', 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 assign_task_to_owner(name, msg, users): - for d in users: - args = { - 'doctype' : 'Subscription', - 'assign_to' : d, - 'name' : name, - 'description' : msg, - 'priority' : 'High' - } - assign_to.add(args) @frappe.whitelist() -def make_subscription(doctype, docname): - doc = frappe.new_doc('Subscription') +def cancel_subscription(name): + """ + Cancels a `Subscription`. This will stop the `Subscription` from further invoicing the + `Subscriber` but all already outstanding invoices will not be affected. + """ + subscription = frappe.get_doc('Subscription', name) + subscription.cancel_subscription() - reference_doc = frappe.get_doc(doctype, docname) - doc.reference_doctype = doctype - doc.reference_document = docname - doc.start_date = reference_doc.get('posting_date') or reference_doc.get('transaction_date') - return doc @frappe.whitelist() -def stop_resume_subscription(subscription, status): - doc = frappe.get_doc('Subscription', subscription) - frappe.msgprint(_("Subscription has been {0}").format(status)) - if status == 'Resumed': - doc.next_schedule_date = get_next_schedule_date(today(), - doc.frequency, doc.repeat_on_day) +def restart_subscription(name): + """ + Restarts a cancelled `Subscription`. The `Subscription` will 'forget' the history of + all invoices it has generated + """ + subscription = frappe.get_doc('Subscription', name) + subscription.restart_subscription() - doc.update_status(status) - doc.save() - return doc.status - -def subscription_doctype_query(doctype, txt, searchfield, start, page_len, filters): - return frappe.db.sql("""select parent from `tabDocField` - where fieldname = 'subscription' - and parent like %(txt)s - order by - if(locate(%(_txt)s, parent), locate(%(_txt)s, parent), 99999), - parent - limit %(start)s, %(page_len)s""".format(**{ - 'key': searchfield, - }), { - 'txt': "%%%s%%" % txt, - '_txt': txt.replace("%", ""), - 'start': start, - 'page_len': page_len - }) \ No newline at end of file +@frappe.whitelist() +def get_subscription_updates(name): + """ + Use this to get the latest state of the given `Subscription` + """ + subscription = frappe.get_doc('Subscription', name) + subscription.process() diff --git a/erpnext/accounts/doctype/subscription/subscription_list.js b/erpnext/accounts/doctype/subscription/subscription_list.js index 71e3cce79d..6614e24c69 100644 --- a/erpnext/accounts/doctype/subscription/subscription_list.js +++ b/erpnext/accounts/doctype/subscription/subscription_list.js @@ -1,16 +1,15 @@ frappe.listview_settings['Subscription'] = { - add_fields: ["next_schedule_date"], get_indicator: function(doc) { - if(doc.disabled) { - return [__("Disabled"), "red"]; - } else if(doc.next_schedule_date >= frappe.datetime.get_today() && doc.status != 'Stopped') { + if(doc.status === 'Trialling') { + return [__("Trialling"), "green"]; + } else if(doc.status === 'Active') { return [__("Active"), "green"]; - } else if(doc.docstatus === 0) { - return [__("Draft"), "red", "docstatus,=,0"]; - } else if(doc.status === 'Stopped') { - return [__("Stopped"), "red"]; - } else { - return [__("Expired"), "darkgrey"]; + } else if(doc.status === 'Past Due Date') { + return [__("Past Due Date"), "orange"]; + } else if(doc.status === 'Unpaid') { + return [__("Unpaid"), "red"]; + } else if(doc.status === 'Canceled') { + return [__("Canceled"), "darkgrey"]; } } }; \ No newline at end of file diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py index 118188c29c..781567da51 100644 --- a/erpnext/accounts/doctype/subscription/test_subscription.py +++ b/erpnext/accounts/doctype/subscription/test_subscription.py @@ -1,93 +1,498 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt from __future__ import unicode_literals -import frappe import unittest -from frappe.utils import today, add_days, getdate -from erpnext.accounts.utils import get_fiscal_year -from erpnext.accounts.report.financial_statements import get_months -from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice -from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order -from erpnext.accounts.doctype.subscription.subscription import make_subscription_entry + +import frappe +from erpnext.accounts.doctype.subscription.subscription import get_prorata_factor +from frappe.utils.data import nowdate, add_days, add_to_date, add_months, date_diff, flt + + +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.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(): + if not frappe.db.exists('Subscriber', '_Test Customer'): + subscriber = frappe.new_doc('Subscriber') + subscriber.subscriber_name = '_Test Customer' + subscriber.customer = '_Test Customer' + subscriber.insert() + class TestSubscription(unittest.TestCase): - def test_daily_subscription(self): - qo = frappe.copy_doc(quotation_records[0]) - qo.submit() - doc = make_subscription(reference_document=qo.name) - self.assertEqual(doc.next_schedule_date, today()) - make_subscription_entry() - frappe.db.commit() + def setUp(self): + create_plan() + create_subscriber() - quotation = frappe.get_doc(doc.reference_doctype, doc.reference_document) - self.assertEqual(quotation.subscription, doc.name) + def test_create_subscription_with_trial_with_correct_period(self): + subscription = frappe.new_doc('Subscription') + 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() - new_quotation = frappe.db.get_value('Quotation', - {'subscription': doc.name, 'name': ('!=', quotation.name)}, 'name') + 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') - new_quotation = frappe.get_doc('Quotation', new_quotation) + subscription.delete() - for fieldname in ['customer', 'company', 'order_type', 'total', 'net_total']: - self.assertEqual(quotation.get(fieldname), new_quotation.get(fieldname)) + def test_create_subscription_without_trial_with_correct_period(self): + subscription = frappe.new_doc('Subscription') + subscription.subscriber = '_Test Customer' + subscription.append('plans', {'plan': '_Test Plan Name'}) + subscription.save() - for fieldname in ['item_code', 'qty', 'rate', 'amount']: - self.assertEqual(quotation.items[0].get(fieldname), - new_quotation.items[0].get(fieldname)) + 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') - def test_monthly_subscription_for_so(self): - current_fiscal_year = get_fiscal_year(today(), as_dict=True) - start_date = current_fiscal_year.year_start_date - end_date = current_fiscal_year.year_end_date + subscription.delete() - for doctype in ['Sales Order', 'Sales Invoice']: - if doctype == 'Sales Invoice': - docname = create_sales_invoice(posting_date=start_date) - else: - docname = make_sales_order() + def test_create_subscription_trial_with_wrong_dates(self): + subscription = frappe.new_doc('Subscription') + 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.monthly_subscription(doctype, docname.name, start_date, end_date) + self.assertRaises(frappe.ValidationError, subscription.save) + subscription.delete() - def monthly_subscription(self, doctype, docname, start_date, end_date): - doc = make_subscription(reference_doctype=doctype, frequency = 'Monthly', - reference_document = docname, start_date=start_date, end_date=end_date) + def test_create_subscription_multi_with_different_billing_fails(self): + subscription = frappe.new_doc('Subscription') + 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'}) - doc.disabled = 1 - doc.save() - frappe.db.commit() + self.assertRaises(frappe.ValidationError, subscription.save) + subscription.delete() - make_subscription_entry() - docnames = frappe.get_all(doc.reference_doctype, {'subscription': doc.name}) - self.assertEqual(len(docnames), 1) + def test_invoice_is_generated_at_end_of_billing_period(self): + subscription = frappe.new_doc('Subscription') + subscription.subscriber = '_Test Customer' + subscription.start = '2018-01-01' + subscription.append('plans', {'plan': '_Test Plan Name'}) + subscription.insert() - doc = frappe.get_doc('Subscription', doc.name) - doc.disabled = 0 - doc.save() + 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() - months = get_months(getdate(start_date), getdate(today())) - make_subscription_entry() + self.assertEqual(len(subscription.invoices), 1) + self.assertEqual(subscription.current_invoice_start, '2018-01-01') + self.assertEqual(subscription.status, 'Past Due Date') + subscription.delete() - docnames = frappe.get_all(doc.reference_doctype, {'subscription': doc.name}) - self.assertEqual(len(docnames), months) + def test_status_goes_back_to_active_after_invoice_is_paid(self): + subscription = frappe.new_doc('Subscription') + 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') -quotation_records = frappe.get_test_records('Quotation') + subscription.get_current_invoice() + current_invoice = subscription.get_current_invoice() -def make_subscription(**args): - args = frappe._dict(args) - doc = frappe.get_doc({ - 'doctype': 'Subscription', - 'reference_doctype': args.reference_doctype or 'Quotation', - 'reference_document': args.reference_document or \ - frappe.db.get_value('Quotation', {'docstatus': 1}, 'name'), - 'frequency': args.frequency or 'Daily', - 'start_date': args.start_date or add_days(today(), -1), - 'end_date': args.end_date or add_days(today(), 1), - 'submit_on_creation': args.submit_on_creation or 0 - }).insert(ignore_permissions=True) + self.assertIsNotNone(current_invoice) - if not args.do_not_submit: - doc.submit() + current_invoice.db_set('outstanding_amount', 0) + current_invoice.db_set('status', 'Paid') + subscription.process() - return doc \ No newline at end of file + 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('Subscription') + 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('Subscription') + 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('Subscription') + 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('Subscription') + 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('Subscription') + 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('Subscription') + 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): + settings = frappe.get_single('Subscription Settings') + to_prorate = settings.prorate + settings.prorate = 1 + settings.save() + + subscription = frappe.new_doc('Subscription') + 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 = flt(date_diff(nowdate(), subscription.current_invoice_start) + 1) + plan_days = flt(date_diff(subscription.current_invoice_end, subscription.current_invoice_start) + 1) + prorate_factor = flt(diff/plan_days) + + self.assertEqual( + flt( + get_prorata_factor(subscription.current_invoice_end, subscription.current_invoice_start), + 2), + flt(prorate_factor, 2) + ) + self.assertEqual(flt(invoice.grand_total, 2), flt(prorate_factor * 900, 2)) + self.assertEqual(subscription.status, 'Canceled') + + subscription.delete() + settings.prorate = to_prorate + settings.save() + + 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('Subscription') + 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('Subscription') + subscription.subscriber = '_Test Customer' + subscription.append('plans', {'plan': '_Test Plan Name'}) + subscription.save() + subscription.cancel_subscription() + + invoice = subscription.get_current_invoice() + diff = flt(date_diff(nowdate(), subscription.current_invoice_start) + 1) + plan_days = flt(date_diff(subscription.current_invoice_end, subscription.current_invoice_start) + 1) + prorate_factor = flt(diff / plan_days) + + self.assertEqual(flt(invoice.grand_total, 2), flt(prorate_factor * 900, 2)) + + 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('Subscription') + 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('Subscription') + 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('Subscription') + 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() + + def test_restart_active_subscription(self): + subscription = frappe.new_doc('Subscription') + subscription.subscriber = '_Test Customer' + subscription.append('plans', {'plan': '_Test Plan Name'}) + subscription.save() + + self.assertRaises(frappe.ValidationError, subscription.restart_subscription) + + subscription.delete() + + def test_subscription_invoice_discount_percentage(self): + subscription = frappe.new_doc('Subscription') + subscription.subscriber = '_Test Customer' + subscription.additional_discount_percentage = 10 + subscription.append('plans', {'plan': '_Test Plan Name'}) + subscription.save() + subscription.cancel_subscription() + + invoice = subscription.get_current_invoice() + + self.assertEqual(invoice.additional_discount_percentage, 10) + self.assertEqual(invoice.apply_discount_on, 'Grand Total') + + subscription.delete() + + def test_subscription_invoice_discount_amount(self): + subscription = frappe.new_doc('Subscription') + subscription.subscriber = '_Test Customer' + subscription.additional_discount_amount = 11 + subscription.append('plans', {'plan': '_Test Plan Name'}) + subscription.save() + subscription.cancel_subscription() + + invoice = subscription.get_current_invoice() + + self.assertEqual(invoice.discount_amount, 11) + self.assertEqual(invoice.apply_discount_on, 'Grand Total') + + subscription.delete() diff --git a/erpnext/accounts/doctype/subscriptions/subscriptions.js b/erpnext/accounts/doctype/subscriptions/subscriptions.js deleted file mode 100644 index 92cb93fcc5..0000000000 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.js +++ /dev/null @@ -1,78 +0,0 @@ -// 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 deleted file mode 100644 index ae324cfd76..0000000000 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.json +++ /dev/null @@ -1,786 +0,0 @@ -{ - "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, - "fieldname": "cb_1", - "fieldtype": "Column 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, - "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, - "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": "sb_4", - "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": "Plans", - "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": "cb_2", - "fieldtype": "Column 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, - "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_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, - "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-09 09:31:16.285992", - "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 deleted file mode 100644 index a193e69d67..0000000000 --- a/erpnext/accounts/doctype/subscriptions/subscriptions.py +++ /dev/null @@ -1,499 +0,0 @@ -# -*- 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, flt - - -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.discount_amount = self.additional_discount_amount - - if self.additional_discount_percentage or self.additional_discount_amount: - discount_on = self.apply_additional_discount - invoice.apply_additional_discount = discount_on if discount_on else '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. - """ - if self.status != 'Canceled': - 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. - """ - if self.status == 'Canceled': - self.status = 'Active' - self.db_set('start', nowdate()) - self.update_subscription_period(nowdate()) - self.invoices = [] - self.save() - else: - frappe.throw(_('You cannot restart a Subscription that is not cancelled.')) - - def get_precision(self): - invoice = self.get_current_invoice() - if invoice: - return invoice.precision('grand_total') - - -def get_prorata_factor(period_end, period_start): - diff = flt(date_diff(nowdate(), period_start) + 1) - plan_days = flt(date_diff(period_end, period_start) + 1) - prorate_factor = diff / plan_days - - return prorate_factor - - -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() - - -@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 deleted file mode 100644 index b5fe4efd28..0000000000 --- a/erpnext/accounts/doctype/subscriptions/test_subscriptions.js +++ /dev/null @@ -1,23 +0,0 @@ -/* 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 deleted file mode 100644 index 75c2bf2a74..0000000000 --- a/erpnext/accounts/doctype/subscriptions/test_subscriptions.py +++ /dev/null @@ -1,498 +0,0 @@ -# -*- 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, flt - - -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.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(): - if not frappe.db.exists('Subscriber', '_Test Customer'): - subscriber = frappe.new_doc('Subscriber') - subscriber.subscriber_name = '_Test Customer' - subscriber.customer = '_Test Customer' - subscriber.insert() - - -class TestSubscriptions(unittest.TestCase): - - def setUp(self): - create_plan() - 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): - 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() - - 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 = flt(date_diff(nowdate(), subscription.current_invoice_start) + 1) - plan_days = flt(date_diff(subscription.current_invoice_end, subscription.current_invoice_start) + 1) - prorate_factor = flt(diff/plan_days) - - self.assertEqual( - flt( - get_prorata_factor(subscription.current_invoice_end, subscription.current_invoice_start), - 2), - flt(prorate_factor, 2) - ) - self.assertEqual(flt(invoice.grand_total, 2), flt(prorate_factor * 900, 2)) - self.assertEqual(subscription.status, 'Canceled') - - subscription.delete() - settings.prorate = to_prorate - settings.save() - - 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 = flt(date_diff(nowdate(), subscription.current_invoice_start) + 1) - plan_days = flt(date_diff(subscription.current_invoice_end, subscription.current_invoice_start) + 1) - prorate_factor = flt(diff / plan_days) - - self.assertEqual(flt(invoice.grand_total, 2), flt(prorate_factor * 900, 2)) - - 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() - - def test_restart_active_subscription(self): - subscription = frappe.new_doc('Subscriptions') - subscription.subscriber = '_Test Customer' - subscription.append('plans', {'plan': '_Test Plan Name'}) - subscription.save() - - self.assertRaises(frappe.ValidationError, subscription.restart_subscription) - - subscription.delete() - - def test_subscription_invoice_discount_percentage(self): - subscription = frappe.new_doc('Subscriptions') - subscription.subscriber = '_Test Customer' - subscription.additional_discount_percentage = 10 - subscription.append('plans', {'plan': '_Test Plan Name'}) - subscription.save() - subscription.cancel_subscription() - - invoice = subscription.get_current_invoice() - - self.assertEqual(invoice.additional_discount_percentage, 10) - self.assertEqual(invoice.apply_discount_on, 'Grand Total') - - subscription.delete() - - def test_subscription_invoice_discount_amount(self): - subscription = frappe.new_doc('Subscriptions') - subscription.subscriber = '_Test Customer' - subscription.additional_discount_amount = 11 - subscription.append('plans', {'plan': '_Test Plan Name'}) - subscription.save() - subscription.cancel_subscription() - - invoice = subscription.get_current_invoice() - - self.assertEqual(invoice.discount_amount, 11) - self.assertEqual(invoice.apply_discount_on, 'Grand Total') - - subscription.delete() diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index 09efa2768f..1cc95443c2 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -3258,6 +3258,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3457,7 +3458,7 @@ "bold": 0, "collapsible": 0, "columns": 0, - "fieldname": "subscription", + "fieldname": "auto_repeat", "fieldtype": "Link", "hidden": 0, "ignore_user_permissions": 0, @@ -3466,10 +3467,10 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Subscription", + "label": "Auto Repeat", "length": 0, "no_copy": 1, - "options": "Subscription", + "options": "Auto Repeat", "permlevel": 0, "precision": "", "print_hide": 1, @@ -3495,7 +3496,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-02-21 14:26:17.099336", + "modified": "2018-03-10 07:29:43.181140", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json index b94710fa2a..722c2d2504 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json @@ -40,6 +40,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -71,6 +72,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -103,6 +105,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 1, + "translatable": 0, "unique": 0 }, { @@ -136,6 +139,7 @@ "reqd": 1, "search_index": 1, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -166,6 +170,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -196,6 +201,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "50%" }, @@ -229,6 +235,7 @@ "reqd": 1, "search_index": 1, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -261,6 +268,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -294,6 +302,7 @@ "reqd": 1, "search_index": 1, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -324,6 +333,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -354,6 +364,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -384,6 +395,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -413,6 +425,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -442,6 +455,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -471,6 +485,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -501,6 +516,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -531,6 +547,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -563,6 +580,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -596,6 +614,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -625,6 +644,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "50%" }, @@ -656,6 +676,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -687,6 +708,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -718,6 +740,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -747,6 +770,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -778,6 +802,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -810,6 +835,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -841,6 +867,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -869,6 +896,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -900,6 +928,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -932,6 +961,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -960,6 +990,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -991,6 +1022,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1023,6 +1055,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1053,6 +1086,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1084,6 +1118,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1117,6 +1152,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1146,6 +1182,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1177,6 +1214,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1206,6 +1244,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1238,6 +1277,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1268,6 +1308,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1298,6 +1339,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1329,6 +1371,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1361,6 +1404,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1393,6 +1437,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1425,6 +1470,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1454,6 +1500,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1486,6 +1533,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1518,6 +1566,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1549,6 +1598,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1580,6 +1630,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1612,6 +1663,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1643,6 +1695,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1672,6 +1725,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1702,6 +1756,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1733,6 +1788,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1762,6 +1818,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1794,6 +1851,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1825,6 +1883,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1857,6 +1916,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1889,6 +1949,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1918,6 +1979,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1950,6 +2012,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1981,6 +2044,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2012,6 +2076,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2044,6 +2109,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2076,6 +2142,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2107,6 +2174,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2137,6 +2205,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2169,6 +2238,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2201,6 +2271,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2231,6 +2302,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2248,7 +2320,7 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Subscription Section", + "label": "Auto Repeat Section", "length": 0, "no_copy": 0, "permlevel": 0, @@ -2261,6 +2333,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2269,7 +2342,7 @@ "bold": 0, "collapsible": 0, "columns": 0, - "fieldname": "subscription", + "fieldname": "auto_repeat", "fieldtype": "Link", "hidden": 0, "ignore_user_permissions": 0, @@ -2278,10 +2351,10 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Subscription", + "label": "Auto Repeat", "length": 0, "no_copy": 1, - "options": "Subscription", + "options": "Auto Repeat", "permlevel": 0, "precision": "", "print_hide": 1, @@ -2292,6 +2365,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2323,6 +2397,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2355,6 +2430,7 @@ "reqd": 1, "search_index": 1, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2383,6 +2459,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2414,6 +2491,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2444,6 +2522,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2475,6 +2554,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 } ], @@ -2490,7 +2570,7 @@ "istable": 0, "max_attachments": 0, "menu_index": 0, - "modified": "2017-11-29 14:07:56.698355", + "modified": "2018-03-10 07:37:33.662363", "modified_by": "Administrator", "module": "Buying", "name": "Supplier Quotation", diff --git a/erpnext/hooks.py b/erpnext/hooks.py index de70489a87..76adc9bd52 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -217,9 +217,8 @@ doc_events = { 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.assets.doctype.subscriptions.subscriptions.process_all" + "erpnext.accounts.doctype.subscription.subscription.process_all" ], "daily": [ "erpnext.stock.reorder_item.reorder_item", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index e049a81d29..2fef48def8 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -509,3 +509,5 @@ erpnext.patches.v10_0.update_status_for_multiple_source_in_po erpnext.patches.v10_0.set_auto_created_serial_no_in_stock_entry erpnext.patches.v10_0.update_territory_and_customer_group erpnext.patches.v10_0.update_warehouse_address_details +erpnext.patches.v10_1.transfer_subscription_to_auto_repeat +erpnext.patches.v10_1.drop_old_subscription_records diff --git a/erpnext/accounts/doctype/subscriptions/__init__.py b/erpnext/patches/v10_1/__init__.py similarity index 100% rename from erpnext/accounts/doctype/subscriptions/__init__.py rename to erpnext/patches/v10_1/__init__.py diff --git a/erpnext/patches/v10_1/drop_old_subscription_records.py b/erpnext/patches/v10_1/drop_old_subscription_records.py new file mode 100644 index 0000000000..11833263d1 --- /dev/null +++ b/erpnext/patches/v10_1/drop_old_subscription_records.py @@ -0,0 +1,12 @@ +from __future__ import unicode_literals +import frappe + + +def execute(): + subscriptions = frappe.db.sql('SELECT name from `tabSubscription`', as_dict=True) + + for subscription in subscriptions: + doc = frappe.get_doc('Subscription', subscription['name']) + if doc.docstatus == 1: + doc.cancel() + frappe.delete_doc('Subscription', subscription['name'], force=True, ignore_permissions=True) diff --git a/erpnext/patches/v10_1/transfer_subscription_to_auto_repeat.py b/erpnext/patches/v10_1/transfer_subscription_to_auto_repeat.py new file mode 100644 index 0000000000..25a35f2bf3 --- /dev/null +++ b/erpnext/patches/v10_1/transfer_subscription_to_auto_repeat.py @@ -0,0 +1,31 @@ +from __future__ import unicode_literals +import frappe +from frappe.model.utils.rename_field import rename_field + + +def execute(): + to_rename = ['Purchase Order', 'Journal Entry', 'Sales Invoice', 'Payment Entry', + 'Delivery Note', 'Purchase Invoice', 'Quotation', 'Sales Order', + 'Purchase Receipt', 'Supplier Quotation'] + + frappe.reload_doc('accounts', 'doctype', 'sales_invoice') + frappe.reload_doc('accounts', 'doctype', 'purchase_invoice') + frappe.reload_doc('accounts', 'doctype', 'payment_entry') + frappe.reload_doc('accounts', 'doctype', 'journal_entry') + frappe.reload_doc('buying', 'doctype', 'purchase_order') + frappe.reload_doc('buying', 'doctype', 'supplier_quotation') + frappe.reload_doc('desk', 'doctype', 'Auto Repeat') + frappe.reload_doc('selling', 'doctype', 'quotation') + frappe.reload_doc('selling', 'doctype', 'sales_order') + frappe.reload_doc('stock', 'doctype', 'purchase_receipt') + frappe.reload_doc('stock', 'doctype', 'delivery_note') + + for doctype in to_rename: + rename_field(doctype, 'subscription', 'auto_repeat') + + subscriptions = frappe.db.sql('select * from `tabSubscription`', as_dict=1) + + for doc in subscriptions: + doc['doctype'] = 'Auto Repeat' + auto_repeat = frappe.get_doc(doc) + auto_repeat.insert() diff --git a/erpnext/patches/v8_1/gst_fixes.py b/erpnext/patches/v8_1/gst_fixes.py index b47879c08d..b343b1d48f 100644 --- a/erpnext/patches/v8_1/gst_fixes.py +++ b/erpnext/patches/v8_1/gst_fixes.py @@ -39,8 +39,8 @@ def add_custom_fields(): custom_fields = { 'Address': [ - dict(fieldname='gst_state_number', label='GST State Number', - fieldtype='Int', insert_after='gst_state'), + dict(fieldname='auto_repeat', label='Auto Repeat', + fieldtype='Link', insert_after='gst_state'), ], 'Sales Invoice': [ dict(fieldname='invoice_copy', label='Invoice Copy', diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json index 0d4835ebdb..b58cd4ef1f 100644 --- a/erpnext/selling/doctype/quotation/quotation.json +++ b/erpnext/selling/doctype/quotation/quotation.json @@ -40,6 +40,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -71,6 +72,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -103,6 +105,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 1, + "translatable": 0, "unique": 0 }, { @@ -136,6 +139,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -169,6 +173,7 @@ "reqd": 0, "search_index": 1, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -202,6 +207,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -232,6 +238,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -261,6 +268,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "50%" }, @@ -294,6 +302,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "150px" }, @@ -328,6 +337,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "150px" }, @@ -361,6 +371,7 @@ "reqd": 1, "search_index": 1, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "100px" }, @@ -392,6 +403,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -425,6 +437,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -457,6 +470,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -487,6 +501,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -518,6 +533,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -551,6 +567,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -580,6 +597,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -609,6 +627,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -639,6 +658,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -668,6 +688,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "50%" }, @@ -699,6 +720,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -728,6 +750,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -762,6 +785,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -793,6 +817,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -823,6 +848,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -855,6 +881,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "100px" }, @@ -889,6 +916,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "100px" }, @@ -918,6 +946,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "50%" }, @@ -951,6 +980,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "100px" }, @@ -982,6 +1012,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1013,6 +1044,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1042,6 +1074,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1073,6 +1106,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1105,6 +1139,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "40px" }, @@ -1134,6 +1169,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1165,6 +1201,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1197,6 +1234,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "100px" }, @@ -1226,6 +1264,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1257,6 +1296,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1287,6 +1327,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1317,6 +1358,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1348,6 +1390,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1380,6 +1423,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1408,6 +1452,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1439,6 +1484,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1467,6 +1513,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1499,6 +1546,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1529,6 +1577,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1559,6 +1608,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1587,6 +1637,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1619,6 +1670,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1647,6 +1699,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1677,6 +1730,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1708,6 +1762,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1740,6 +1795,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1771,6 +1827,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1800,6 +1857,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1830,6 +1888,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1860,6 +1919,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1891,6 +1951,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1923,6 +1984,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "200px" }, @@ -1955,6 +2017,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1987,6 +2050,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "200px" }, @@ -2020,6 +2084,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "200px" }, @@ -2050,6 +2115,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "50%" }, @@ -2083,6 +2149,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "200px" }, @@ -2115,6 +2182,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2147,6 +2215,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "200px" }, @@ -2179,6 +2248,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "200px" }, @@ -2212,6 +2282,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2243,6 +2314,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2274,6 +2346,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2306,6 +2379,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2338,6 +2412,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2369,6 +2444,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2399,6 +2475,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2431,6 +2508,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2463,6 +2541,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2492,6 +2571,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2522,6 +2602,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2539,7 +2620,7 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Subscription Section", + "label": "Auto Repeat Section", "length": 0, "no_copy": 0, "permlevel": 0, @@ -2552,6 +2633,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2560,7 +2642,7 @@ "bold": 0, "collapsible": 0, "columns": 0, - "fieldname": "subscription", + "fieldname": "auto_repeat", "fieldtype": "Link", "hidden": 0, "ignore_user_permissions": 0, @@ -2569,10 +2651,10 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Subscription", + "label": "Auto Repeat", "length": 0, "no_copy": 1, - "options": "Subscription", + "options": "Auto Repeat", "permlevel": 0, "precision": "", "print_hide": 1, @@ -2583,6 +2665,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2614,6 +2697,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2646,6 +2730,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2678,6 +2763,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2710,6 +2796,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2739,6 +2826,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "50%" }, @@ -2773,6 +2861,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2804,6 +2893,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2835,6 +2925,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2866,6 +2957,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 } ], @@ -2881,7 +2973,7 @@ "istable": 0, "max_attachments": 1, "menu_index": 0, - "modified": "2018-01-11 14:42:22.052380", + "modified": "2018-03-10 07:34:23.971888", "modified_by": "Administrator", "module": "Selling", "name": "Quotation", diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 6d9c170aef..dbb1e9f86e 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -103,7 +103,7 @@ class Quotation(SellingController): print_lst.append(lst1) return print_lst - def on_recurring(self, reference_doc, subscription_doc): + def on_recurring(self, reference_doc, auto_repeat_doc): self.valid_till = None def get_list_context(context=None): diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index f302c1cd1c..221eca78ff 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -41,6 +41,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -70,6 +71,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "50%" }, @@ -102,6 +104,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -134,6 +137,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 1, + "translatable": 0, "unique": 0 }, { @@ -166,6 +170,7 @@ "reqd": 1, "search_index": 1, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -196,6 +201,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -230,6 +236,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -259,6 +266,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "50%" }, @@ -292,6 +300,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "150px" }, @@ -326,6 +335,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "150px" }, @@ -359,6 +369,7 @@ "reqd": 1, "search_index": 1, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "160px" }, @@ -390,6 +401,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -423,6 +435,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "100px" }, @@ -457,6 +470,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "100px" }, @@ -488,6 +502,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "100px" }, @@ -521,6 +536,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -551,6 +567,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -580,6 +597,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -610,6 +628,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -639,6 +658,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -668,6 +688,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -698,6 +719,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -726,6 +748,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "50%" }, @@ -757,6 +780,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -786,6 +810,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -817,6 +842,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -848,6 +874,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -878,6 +905,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -910,6 +938,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "100px" }, @@ -944,6 +973,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "100px" }, @@ -973,6 +1003,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "50%" }, @@ -1006,6 +1037,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "100px" }, @@ -1037,6 +1069,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1068,6 +1101,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1097,6 +1131,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1128,6 +1163,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1160,6 +1196,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1188,6 +1225,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1216,6 +1254,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1247,6 +1286,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1279,6 +1319,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "150px" }, @@ -1308,6 +1349,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1339,6 +1381,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1369,6 +1412,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1399,6 +1443,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1430,6 +1475,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1462,6 +1508,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1490,6 +1537,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1521,6 +1569,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1549,6 +1598,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1581,6 +1631,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1611,6 +1662,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1641,6 +1693,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1669,6 +1722,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1701,6 +1755,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "150px" }, @@ -1731,6 +1786,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1761,6 +1817,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1792,6 +1849,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1824,6 +1882,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1855,6 +1914,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1883,6 +1943,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1914,6 +1975,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1945,6 +2007,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1976,6 +2039,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2008,6 +2072,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "150px" }, @@ -2040,6 +2105,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2072,6 +2138,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "150px" }, @@ -2105,6 +2172,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "200px" }, @@ -2135,6 +2203,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "50%" }, @@ -2168,6 +2237,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "150px" }, @@ -2201,6 +2271,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2233,6 +2304,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "150px" }, @@ -2265,6 +2337,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "200px" }, @@ -2296,6 +2369,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2329,6 +2403,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2360,6 +2435,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2391,6 +2467,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2422,6 +2499,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2453,6 +2531,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2485,6 +2564,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2517,6 +2597,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2548,6 +2629,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2580,6 +2662,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2613,6 +2696,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2644,6 +2728,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2673,6 +2758,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2705,6 +2791,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2738,6 +2825,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2768,6 +2856,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2798,6 +2887,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2830,6 +2920,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2859,6 +2950,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "50%" }, @@ -2892,6 +2984,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2922,6 +3015,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2952,6 +3046,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "50%" }, @@ -2986,6 +3081,7 @@ "reqd": 1, "search_index": 1, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "100px" }, @@ -3017,6 +3113,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3050,6 +3147,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "100px" }, @@ -3079,6 +3177,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3112,6 +3211,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "100px" }, @@ -3143,6 +3243,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3175,6 +3276,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3207,6 +3309,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "150px" }, @@ -3236,6 +3339,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "50%" }, @@ -3268,6 +3372,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "100px" }, @@ -3301,6 +3406,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3331,6 +3437,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3363,6 +3470,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3380,7 +3488,7 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Subscription Section", + "label": "Auto Repeat Section", "length": 0, "no_copy": 1, "permlevel": 0, @@ -3393,6 +3501,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3424,6 +3533,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3455,6 +3565,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3484,6 +3595,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3492,7 +3604,7 @@ "bold": 0, "collapsible": 0, "columns": 0, - "fieldname": "subscription", + "fieldname": "auto_repeat", "fieldtype": "Link", "hidden": 0, "ignore_user_permissions": 0, @@ -3501,10 +3613,10 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Subscription", + "label": "Auto Repeat", "length": 0, "no_copy": 0, - "options": "Subscription", + "options": "Auto Repeat", "permlevel": 0, "precision": "", "print_hide": 0, @@ -3515,6 +3627,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 } ], @@ -3529,7 +3642,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-01-12 15:56:12.483019", + "modified": "2018-03-10 07:35:33.258624", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 4208b08d87..6f2caebc38 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -13,7 +13,7 @@ from erpnext.stock.stock_balance import update_bin_qty, get_reserved_qty from frappe.desk.notifications import clear_doctype_notifications from frappe.contacts.doctype.address.address import get_company_address from erpnext.controllers.selling_controller import SellingController -from erpnext.accounts.doctype.subscription.subscription import get_next_schedule_date +from frappe.desk.doctype.auto_repeat.auto_repeat import get_next_schedule_date from erpnext.selling.doctype.customer.customer import check_credit_limit form_grid_templates = { @@ -368,16 +368,16 @@ class SalesOrder(SellingController): )) return items - def on_recurring(self, reference_doc, subscription_doc): + def on_recurring(self, reference_doc, auto_repeat_doc): self.set("delivery_date", get_next_schedule_date(reference_doc.delivery_date, - subscription_doc.frequency, cint(subscription_doc.repeat_on_day))) + auto_repeat_doc.frequency, cint(auto_repeat_doc.repeat_on_day))) for d in self.get("items"): reference_delivery_date = frappe.db.get_value("Sales Order Item", {"parent": reference_doc.name, "item_code": d.item_code, "idx": d.idx}, "delivery_date") d.set("delivery_date", get_next_schedule_date(reference_delivery_date, - subscription_doc.frequency, cint(subscription_doc.repeat_on_day))) + auto_repeat_doc.frequency, cint(auto_repeat_doc.repeat_on_day))) def get_list_context(context=None): from erpnext.controllers.website_list_for_contact import get_list_context diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index 04639416a9..a427a351fd 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -40,6 +40,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -70,6 +71,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "50%" }, @@ -102,6 +104,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -134,6 +137,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 1, + "translatable": 0, "unique": 0 }, { @@ -166,6 +170,7 @@ "reqd": 1, "search_index": 1, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -197,6 +202,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -226,6 +232,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -259,6 +266,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "150px" }, @@ -294,6 +302,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "150px" }, @@ -312,7 +321,7 @@ "ignore_xss_filter": 0, "in_filter": 0, "in_global_search": 0, - "in_list_view": 1, + "in_list_view": 0, "in_standard_filter": 0, "label": "Date", "length": 0, @@ -329,6 +338,7 @@ "reqd": 1, "search_index": 1, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "100px" }, @@ -364,6 +374,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "100px" }, @@ -396,6 +407,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -426,6 +438,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -458,6 +471,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -489,6 +503,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -521,6 +536,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "100px" }, @@ -551,6 +567,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -584,6 +601,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "100px" }, @@ -616,6 +634,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -646,6 +665,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -675,6 +695,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -705,6 +726,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -734,6 +756,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -763,6 +786,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -793,6 +817,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -822,6 +847,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "50%" }, @@ -854,6 +880,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -884,6 +911,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -913,6 +941,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -944,6 +973,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -974,6 +1004,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1004,6 +1035,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1036,6 +1068,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1069,6 +1102,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1097,6 +1131,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1129,6 +1164,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1159,6 +1195,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1190,6 +1227,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1219,6 +1257,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1250,6 +1289,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1282,6 +1322,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1314,6 +1355,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1346,6 +1388,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1375,6 +1418,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1403,6 +1447,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1434,6 +1479,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1467,6 +1513,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "150px" }, @@ -1496,6 +1543,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1527,6 +1575,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1557,6 +1606,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1587,6 +1637,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1618,6 +1669,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1651,6 +1703,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1679,6 +1732,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1710,6 +1764,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1738,6 +1793,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1770,6 +1826,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1800,6 +1857,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1830,6 +1888,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1858,6 +1917,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1891,6 +1951,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "150px" }, @@ -1921,6 +1982,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1951,6 +2013,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1982,6 +2045,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2014,6 +2078,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2045,6 +2110,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2073,6 +2139,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2103,6 +2170,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2133,6 +2201,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2164,6 +2233,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2197,6 +2267,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "150px" }, @@ -2229,6 +2300,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2262,6 +2334,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "150px" }, @@ -2296,6 +2369,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "200px" }, @@ -2326,6 +2400,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2359,6 +2434,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "150px" }, @@ -2391,6 +2467,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2424,6 +2501,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "150px" }, @@ -2458,6 +2536,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "150px" }, @@ -2491,95 +2570,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "payment_terms_template_section", - "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, - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "payment_terms_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": "Payment Terms Template", - "length": 0, - "no_copy": 0, - "options": "Payment Terms Template", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "terms_section", - "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, - "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 }, { @@ -2612,6 +2603,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2643,6 +2635,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2674,6 +2667,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2706,6 +2700,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "150px" }, @@ -2736,6 +2731,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "50%" }, @@ -2770,6 +2766,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "100px" }, @@ -2805,6 +2802,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "100px" }, @@ -2837,6 +2835,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2870,6 +2869,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2903,6 +2903,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2935,6 +2936,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2965,6 +2967,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "50%" }, @@ -2996,6 +2999,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3027,6 +3031,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3058,6 +3063,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3088,6 +3094,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3120,6 +3127,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3152,6 +3160,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3182,6 +3191,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3211,6 +3221,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3242,6 +3253,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3271,6 +3283,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3305,6 +3318,7 @@ "reqd": 1, "search_index": 1, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "150px" }, @@ -3339,6 +3353,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3368,6 +3383,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3396,6 +3412,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3429,6 +3446,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3460,6 +3478,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3491,6 +3510,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3521,6 +3541,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3529,7 +3550,7 @@ "bold": 0, "collapsible": 0, "columns": 0, - "fieldname": "subscription", + "fieldname": "auto_repeat", "fieldtype": "Link", "hidden": 0, "ignore_user_permissions": 0, @@ -3538,10 +3559,10 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Subscription", + "label": "Auto Repeat", "length": 0, "no_copy": 1, - "options": "Subscription", + "options": "Auto Repeat", "permlevel": 0, "precision": "", "print_hide": 1, @@ -3552,6 +3573,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3584,6 +3606,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3617,6 +3640,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "150px" }, @@ -3647,6 +3671,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "50%" }, @@ -3680,6 +3705,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "100px" }, @@ -3713,6 +3739,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3743,6 +3770,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3775,6 +3803,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 } ], @@ -3790,7 +3819,7 @@ "istable": 0, "max_attachments": 0, "menu_index": 0, - "modified": "2018-01-12 15:27:44.471335", + "modified": "2018-03-10 07:32:37.932784", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index 9f609beee3..4f9f301b6a 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -40,6 +40,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -70,6 +71,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "50%" }, @@ -102,6 +104,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -135,6 +138,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 1, + "translatable": 0, "unique": 0 }, { @@ -168,6 +172,7 @@ "reqd": 1, "search_index": 1, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "150px" }, @@ -200,6 +205,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -230,6 +236,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -260,6 +267,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "50%" }, @@ -295,6 +303,7 @@ "reqd": 1, "search_index": 1, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "100px" }, @@ -330,6 +339,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "100px" }, @@ -362,6 +372,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -396,6 +407,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "150px" }, @@ -427,6 +439,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -459,6 +472,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -489,6 +503,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -519,6 +534,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -549,6 +565,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -579,6 +596,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -608,6 +626,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -637,6 +656,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -666,6 +686,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -695,6 +716,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -727,6 +749,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -757,6 +780,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -787,6 +811,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -819,6 +844,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -852,6 +878,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -882,6 +909,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "50%" }, @@ -913,6 +941,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -944,6 +973,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -975,6 +1005,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1004,6 +1035,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1035,6 +1067,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1067,6 +1100,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1098,6 +1132,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1127,6 +1162,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1158,6 +1194,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1191,6 +1228,7 @@ "reqd": 1, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "150px" }, @@ -1220,6 +1258,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1251,6 +1290,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1283,6 +1323,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1313,6 +1354,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1345,6 +1387,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1378,6 +1421,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1407,6 +1451,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1438,6 +1483,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1467,6 +1513,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1499,6 +1546,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1529,6 +1577,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1559,6 +1608,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1591,6 +1641,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1623,6 +1674,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1655,6 +1707,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1687,6 +1740,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1716,6 +1770,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "50%" }, @@ -1749,6 +1804,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1781,6 +1837,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1812,6 +1869,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1843,6 +1901,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1875,6 +1934,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1906,6 +1966,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1935,6 +1996,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1965,6 +2027,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -1996,6 +2059,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2025,6 +2089,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2057,6 +2122,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2088,6 +2154,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2120,6 +2187,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2152,6 +2220,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2181,6 +2250,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2213,6 +2283,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2244,6 +2315,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2275,6 +2347,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2307,6 +2380,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2339,6 +2413,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2370,6 +2445,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2403,6 +2479,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2437,6 +2514,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2471,6 +2549,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "50px" }, @@ -2504,6 +2583,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2535,6 +2615,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2566,6 +2647,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2597,6 +2679,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2631,6 +2714,7 @@ "reqd": 1, "search_index": 1, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "150px" }, @@ -2665,6 +2749,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2698,6 +2783,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "150px" }, @@ -2730,6 +2816,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2760,6 +2847,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "50%" }, @@ -2791,6 +2879,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2808,7 +2897,7 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Subscription Detail", + "label": "Auto Repeat Detail", "length": 0, "no_copy": 0, "permlevel": 0, @@ -2821,6 +2910,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2829,7 +2919,7 @@ "bold": 0, "collapsible": 0, "columns": 0, - "fieldname": "subscription", + "fieldname": "auto_repeat", "fieldtype": "Link", "hidden": 0, "ignore_user_permissions": 0, @@ -2838,10 +2928,10 @@ "in_global_search": 0, "in_list_view": 0, "in_standard_filter": 0, - "label": "Subscription", + "label": "Auto Repeat", "length": 0, "no_copy": 1, - "options": "Subscription", + "options": "Auto Repeat", "permlevel": 0, "precision": "", "print_hide": 1, @@ -2852,6 +2942,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2882,6 +2973,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2912,6 +3004,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2944,6 +3037,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -2974,6 +3068,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3006,6 +3101,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "30%" }, @@ -3038,6 +3134,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3067,6 +3164,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3098,6 +3196,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3129,6 +3228,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0 }, { @@ -3158,6 +3258,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "50%" }, @@ -3192,6 +3293,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "100px" }, @@ -3226,6 +3328,7 @@ "reqd": 0, "search_index": 0, "set_only_once": 0, + "translatable": 0, "unique": 0, "width": "100px" } @@ -3242,7 +3345,7 @@ "istable": 0, "max_attachments": 0, "menu_index": 0, - "modified": "2018-01-11 14:40:58.353712", + "modified": "2018-03-10 07:36:31.378606", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", diff --git a/erpnext/templates/emails/recurring_document_failed.html b/erpnext/templates/emails/recurring_document_failed.html deleted file mode 100644 index f9e8c2dafc..0000000000 --- a/erpnext/templates/emails/recurring_document_failed.html +++ /dev/null @@ -1,11 +0,0 @@ -

{{_("Recurring")}} {{ type }} {{ _("Failed")}}

- -

{{_("An error occured while creating recurring")}} {{ type }} {{ name }} {{_("for")}} {{ party }}.

-

{{_("This could be because of some invalid Email Addresses in the")}} {{ type }}.

-

{{_("To stop sending repetitive error notifications from the system, we have checked Disabled field in the subscription")}} {{ subscription}} {{_("for the")}} {{ type }} {{ name }}.

-

{{_("Please correct the")}} {{ type }} {{_('and unchcked Disabled in the')}} {{ subscription }} {{_("for making recurring again.")}}

-
-

{{_("It is necessary to take this action today itself for the above mentioned recurring")}} {{ type }} -{{_('to be generated. If delayed, you will have to manually change the "Repeat on Day of Month" field -of this')}} {{ type }} {{_("for generating the recurring")}} {{ type }} {{_("in the subscription")}} {{ subscription }}.

-

[{{_("This email is autogenerated")}}]

From 5988da9a314a43f5b9dcba002a3c04d50c14acf6 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Tue, 17 Apr 2018 11:30:35 +0100 Subject: [PATCH 70/73] code review: revert changes --- erpnext/patches/v8_1/gst_fixes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/patches/v8_1/gst_fixes.py b/erpnext/patches/v8_1/gst_fixes.py index b343b1d48f..b47879c08d 100644 --- a/erpnext/patches/v8_1/gst_fixes.py +++ b/erpnext/patches/v8_1/gst_fixes.py @@ -39,8 +39,8 @@ def add_custom_fields(): custom_fields = { 'Address': [ - dict(fieldname='auto_repeat', label='Auto Repeat', - fieldtype='Link', insert_after='gst_state'), + dict(fieldname='gst_state_number', label='GST State Number', + fieldtype='Int', insert_after='gst_state'), ], 'Sales Invoice': [ dict(fieldname='invoice_copy', label='Invoice Copy', From 20721942ad3bac1776cf17e983f0b04c2fb47f78 Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Tue, 17 Apr 2018 11:31:23 +0100 Subject: [PATCH 71/73] code review: use `delete from` to delete records --- erpnext/patches/v10_1/drop_old_subscription_records.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/erpnext/patches/v10_1/drop_old_subscription_records.py b/erpnext/patches/v10_1/drop_old_subscription_records.py index 11833263d1..7573f1568f 100644 --- a/erpnext/patches/v10_1/drop_old_subscription_records.py +++ b/erpnext/patches/v10_1/drop_old_subscription_records.py @@ -3,10 +3,4 @@ import frappe def execute(): - subscriptions = frappe.db.sql('SELECT name from `tabSubscription`', as_dict=True) - - for subscription in subscriptions: - doc = frappe.get_doc('Subscription', subscription['name']) - if doc.docstatus == 1: - doc.cancel() - frappe.delete_doc('Subscription', subscription['name'], force=True, ignore_permissions=True) + frappe.db.sql('DELETE from `tabSubscription`') From 280c7a410a751aeb12e96dffb4f50ffb57ef266f Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Tue, 17 Apr 2018 11:32:17 +0100 Subject: [PATCH 72/73] code review: make use of db_insert and change `Auto Repeat` to `auto_repeat` --- erpnext/patches/v10_1/transfer_subscription_to_auto_repeat.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/patches/v10_1/transfer_subscription_to_auto_repeat.py b/erpnext/patches/v10_1/transfer_subscription_to_auto_repeat.py index 25a35f2bf3..cdb013d69b 100644 --- a/erpnext/patches/v10_1/transfer_subscription_to_auto_repeat.py +++ b/erpnext/patches/v10_1/transfer_subscription_to_auto_repeat.py @@ -14,7 +14,7 @@ def execute(): frappe.reload_doc('accounts', 'doctype', 'journal_entry') frappe.reload_doc('buying', 'doctype', 'purchase_order') frappe.reload_doc('buying', 'doctype', 'supplier_quotation') - frappe.reload_doc('desk', 'doctype', 'Auto Repeat') + frappe.reload_doc('desk', 'doctype', 'auto_repeat') frappe.reload_doc('selling', 'doctype', 'quotation') frappe.reload_doc('selling', 'doctype', 'sales_order') frappe.reload_doc('stock', 'doctype', 'purchase_receipt') @@ -28,4 +28,4 @@ def execute(): for doc in subscriptions: doc['doctype'] = 'Auto Repeat' auto_repeat = frappe.get_doc(doc) - auto_repeat.insert() + auto_repeat.db_insert() From f65ee888c0f39476bae70f1dbd718436e7bf0c9c Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Sun, 22 Apr 2018 23:43:08 +0100 Subject: [PATCH 73/73] spelling: canceled --> cancelled --- .../doctype/subscription/subscription.js | 4 ++-- .../doctype/subscription/subscription.json | 2 +- .../doctype/subscription/subscription.py | 18 ++++++++--------- .../doctype/subscription/subscription_list.js | 4 ++-- .../doctype/subscription/test_subscription.py | 20 +++++++++---------- 5 files changed, 24 insertions(+), 24 deletions(-) diff --git a/erpnext/accounts/doctype/subscription/subscription.js b/erpnext/accounts/doctype/subscription/subscription.js index 8d8f2cc9b4..dcbec12f8b 100644 --- a/erpnext/accounts/doctype/subscription/subscription.js +++ b/erpnext/accounts/doctype/subscription/subscription.js @@ -4,7 +4,7 @@ frappe.ui.form.on('Subscription', { refresh: function(frm) { if(!frm.is_new()){ - if(frm.doc.status !== 'Canceled'){ + if(frm.doc.status !== 'Cancelled'){ frm.add_custom_button( __('Cancel Subscription'), () => frm.events.cancel_this_subscription(frm) @@ -14,7 +14,7 @@ frappe.ui.form.on('Subscription', { () => frm.events.get_subscription_updates(frm) ); } - else if(frm.doc.status === 'Canceled'){ + else if(frm.doc.status === 'Cancelled'){ frm.add_custom_button( __('Restart Subscription'), () => frm.events.renew_this_subscription(frm) diff --git a/erpnext/accounts/doctype/subscription/subscription.json b/erpnext/accounts/doctype/subscription/subscription.json index 80cbd85e70..1165bede2d 100644 --- a/erpnext/accounts/doctype/subscription/subscription.json +++ b/erpnext/accounts/doctype/subscription/subscription.json @@ -315,7 +315,7 @@ "label": "Status", "length": 0, "no_copy": 0, - "options": "\nTrialling\nActive\nPast Due Date\nCanceled\nUnpaid", + "options": "\nTrialling\nActive\nPast Due Date\nCancelled\nUnpaid", "permlevel": 0, "precision": "", "print_hide": 0, diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py index 7b013bb7be..8f4fe4d728 100644 --- a/erpnext/accounts/doctype/subscription/subscription.py +++ b/erpnext/accounts/doctype/subscription/subscription.py @@ -129,7 +129,7 @@ class Subscription(Document): """ 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' + self.status = 'Cancelled' if cint(subscription_settings.cancel_after_grace) else 'Unpaid' def set_subscription_status(self): """ @@ -139,7 +139,7 @@ class Subscription(Document): 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' + self.status = 'Cancelled' 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(): @@ -335,7 +335,7 @@ class Subscription(Document): 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' + 3. Change the `Subscription` status to 'Cancelled' """ if getdate(nowdate()) > getdate(self.current_invoice_end) and not self.has_outstanding_invoice(): self.generate_invoice() @@ -352,7 +352,7 @@ class Subscription(Document): """ Called when `Subscription.cancel_at_period_end` is truthy """ - self.status = 'Canceled' + self.status = 'Cancelled' if not self.cancelation_date: self.cancelation_date = nowdate() @@ -362,7 +362,7 @@ class Subscription(Document): The possible outcomes of this method are: 1. Change the `Subscription` status to 'Active' - 2. Change the `Subscription` status to 'Canceled' + 2. Change the `Subscription` status to 'Cancelled' 3. Change the `Subscription` status to 'Unpaid' """ current_invoice = self.get_current_invoice() @@ -397,10 +397,10 @@ class Subscription(Document): This sets the subscription as cancelled. It will stop invoices from being generated but it will not affect already created invoices. """ - if self.status != 'Canceled': + if self.status != 'Cancelled': to_generate_invoice = True if self.status == 'Active' else False to_prorate = frappe.db.get_single_value('Subscription Settings', 'prorate') - self.status = 'Canceled' + self.status = 'Cancelled' self.cancelation_date = nowdate() if to_generate_invoice: self.generate_invoice(prorate=to_prorate) @@ -412,7 +412,7 @@ class Subscription(Document): subscription and the `Subscription` will lose all the history of generated invoices it has. """ - if self.status == 'Canceled': + if self.status == 'Cancelled': self.status = 'Active' self.db_set('start', nowdate()) self.update_subscription_period(nowdate()) @@ -449,7 +449,7 @@ def get_all_subscriptions(): Returns all `Subscription` documents """ return frappe.db.sql( - 'select name from `tabSubscription` where status != "Canceled"', + 'select name from `tabSubscription` where status != "Cancelled"', as_dict=1 ) diff --git a/erpnext/accounts/doctype/subscription/subscription_list.js b/erpnext/accounts/doctype/subscription/subscription_list.js index 6614e24c69..abcfc5e696 100644 --- a/erpnext/accounts/doctype/subscription/subscription_list.js +++ b/erpnext/accounts/doctype/subscription/subscription_list.js @@ -8,8 +8,8 @@ frappe.listview_settings['Subscription'] = { return [__("Past Due Date"), "orange"]; } else if(doc.status === 'Unpaid') { return [__("Unpaid"), "red"]; - } else if(doc.status === 'Canceled') { - return [__("Canceled"), "darkgrey"]; + } else if(doc.status === 'Cancelled') { + return [__("Cancelled"), "darkgrey"]; } } }; \ No newline at end of file diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py index 781567da51..47efa45429 100644 --- a/erpnext/accounts/doctype/subscription/test_subscription.py +++ b/erpnext/accounts/doctype/subscription/test_subscription.py @@ -165,8 +165,8 @@ class TestSubscription(unittest.TestCase): 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') + # This should change status to Cancelled since grace period is 0 + self.assertEqual(subscription.status, 'Cancelled') settings.cancel_after_grace = default_grace_period_action settings.save() @@ -188,7 +188,7 @@ class TestSubscription(unittest.TestCase): self.assertEqual(subscription.status, 'Past Due Date') subscription.process() - # This should change status to Canceled since grace period is 0 + # This should change status to Cancelled since grace period is 0 self.assertEqual(subscription.status, 'Unpaid') settings.cancel_after_grace = default_grace_period_action @@ -270,7 +270,7 @@ class TestSubscription(unittest.TestCase): subscription.save() subscription.cancel_subscription() - self.assertEqual(subscription.status, 'Canceled') + self.assertEqual(subscription.status, 'Cancelled') subscription.delete() @@ -303,7 +303,7 @@ class TestSubscription(unittest.TestCase): flt(prorate_factor, 2) ) self.assertEqual(flt(invoice.grand_total, 2), flt(prorate_factor * 900, 2)) - self.assertEqual(subscription.status, 'Canceled') + self.assertEqual(subscription.status, 'Cancelled') subscription.delete() settings.prorate = to_prorate @@ -371,15 +371,15 @@ class TestSubscription(unittest.TestCase): self.assertEqual(len(subscription.invoices), invoices) subscription.cancel_subscription() - self.assertEqual(subscription.status, 'Canceled') + self.assertEqual(subscription.status, 'Cancelled') self.assertEqual(len(subscription.invoices), invoices) subscription.process() - self.assertEqual(subscription.status, 'Canceled') + self.assertEqual(subscription.status, 'Cancelled') self.assertEqual(len(subscription.invoices), invoices) subscription.process() - self.assertEqual(subscription.status, 'Canceled') + self.assertEqual(subscription.status, 'Cancelled') self.assertEqual(len(subscription.invoices), invoices) settings.cancel_after_grace = default_grace_period_action @@ -406,7 +406,7 @@ class TestSubscription(unittest.TestCase): self.assertEqual(subscription.status, 'Unpaid') subscription.cancel_subscription() - self.assertEqual(subscription.status, 'Canceled') + self.assertEqual(subscription.status, 'Cancelled') subscription.restart_subscription() self.assertEqual(subscription.status, 'Active') @@ -440,7 +440,7 @@ class TestSubscription(unittest.TestCase): self.assertEqual(subscription.status, 'Past Due Date') subscription.process() - # This should change status to Canceled since grace period is 0 + # This should change status to Cancelled since grace period is 0 self.assertEqual(subscription.status, 'Unpaid') invoice = subscription.get_current_invoice()