feat: Enhancement in subscription (#22263)
* feat: Add supplier in subscription doctype
* fix: Code cleanup
* fix: Add dynamic link in subscription invoices
* fix: Multiple enhanccement in subscription
* feat: Follow calendar months in subscription
* fix: Test Cases and patch
* fix: Patch
* fix: Update patch and add fixes
* fix: Update permission for subscription settings
* fix: Patch and Test
* fix: Add cost center dimension in Subscripiton
diff --git a/erpnext/accounts/doctype/subscription/subscription.js b/erpnext/accounts/doctype/subscription/subscription.js
index dcbec12..ba98eb9 100644
--- a/erpnext/accounts/doctype/subscription/subscription.js
+++ b/erpnext/accounts/doctype/subscription/subscription.js
@@ -2,6 +2,16 @@
// For license information, please see license.txt
frappe.ui.form.on('Subscription', {
+ setup: function(frm) {
+ frm.set_query('party_type', function() {
+ return {
+ filters : {
+ name: ['in', ['Customer', 'Supplier']]
+ }
+ }
+ });
+ },
+
refresh: function(frm) {
if(!frm.is_new()){
if(frm.doc.status !== 'Cancelled'){
diff --git a/erpnext/accounts/doctype/subscription/subscription.json b/erpnext/accounts/doctype/subscription/subscription.json
index 32b97ba..afb94fe 100644
--- a/erpnext/accounts/doctype/subscription/subscription.json
+++ b/erpnext/accounts/doctype/subscription/subscription.json
@@ -6,14 +6,18 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
- "customer",
- "cb_1",
+ "party_type",
"status",
+ "cb_1",
+ "party",
"subscription_period",
- "start",
+ "start_date",
+ "end_date",
"cancelation_date",
"trial_period_start",
"trial_period_end",
+ "follow_calendar_months",
+ "generate_new_invoices_past_due_date",
"column_break_11",
"current_invoice_start",
"current_invoice_end",
@@ -23,7 +27,8 @@
"sb_4",
"plans",
"sb_1",
- "tax_template",
+ "sales_tax_template",
+ "purchase_tax_template",
"sb_2",
"apply_additional_discount",
"cb_2",
@@ -32,19 +37,11 @@
"sb_3",
"invoices",
"accounting_dimensions_section",
+ "cost_center",
"dimension_col_break"
],
"fields": [
{
- "fieldname": "customer",
- "fieldtype": "Link",
- "in_list_view": 1,
- "label": "Customer",
- "options": "Customer",
- "reqd": 1,
- "set_only_once": 1
- },
- {
"allow_on_submit": 1,
"fieldname": "cb_1",
"fieldtype": "Column Break"
@@ -53,7 +50,7 @@
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
- "options": "\nTrialling\nActive\nPast Due Date\nCancelled\nUnpaid",
+ "options": "\nTrialling\nActive\nPast Due Date\nCancelled\nUnpaid\nCompleted",
"read_only": 1
},
{
@@ -62,12 +59,6 @@
"label": "Subscription Period"
},
{
- "fieldname": "start",
- "fieldtype": "Date",
- "label": "Subscription Start Date",
- "set_only_once": 1
- },
- {
"fieldname": "cancelation_date",
"fieldtype": "Date",
"label": "Cancelation Date",
@@ -137,17 +128,12 @@
"reqd": 1
},
{
+ "depends_on": "eval:['Customer', 'Supplier'].includes(doc.party_type)",
"fieldname": "sb_1",
"fieldtype": "Section Break",
"label": "Taxes"
},
{
- "fieldname": "tax_template",
- "fieldtype": "Link",
- "label": "Sales Taxes and Charges Template",
- "options": "Sales Taxes and Charges Template"
- },
- {
"fieldname": "sb_2",
"fieldtype": "Section Break",
"label": "Discounts"
@@ -195,10 +181,74 @@
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "party_type",
+ "fieldtype": "Link",
+ "label": "Party Type",
+ "options": "DocType",
+ "reqd": 1,
+ "set_only_once": 1
+ },
+ {
+ "fieldname": "party",
+ "fieldtype": "Dynamic Link",
+ "in_list_view": 1,
+ "label": "Party",
+ "options": "party_type",
+ "reqd": 1,
+ "set_only_once": 1
+ },
+ {
+ "depends_on": "eval:doc.party_type === 'Customer'",
+ "fieldname": "sales_tax_template",
+ "fieldtype": "Link",
+ "label": "Sales Taxes and Charges Template",
+ "options": "Sales Taxes and Charges Template"
+ },
+ {
+ "depends_on": "eval:doc.party_type === 'Supplier'",
+ "fieldname": "purchase_tax_template",
+ "fieldtype": "Link",
+ "label": "Purchase Taxes and Charges Template",
+ "options": "Purchase Taxes and Charges Template"
+ },
+ {
+ "default": "0",
+ "description": "If this is checked subsequent new invoices will be created on calendar month and quarter start dates irrespective of current invoice start date",
+ "fieldname": "follow_calendar_months",
+ "fieldtype": "Check",
+ "label": "Follow Calendar Months",
+ "set_only_once": 1
+ },
+ {
+ "default": "0",
+ "description": "New invoices will be generated as per schedule even if current invoices are unpaid or past due date",
+ "fieldname": "generate_new_invoices_past_due_date",
+ "fieldtype": "Check",
+ "label": "Generate New Invoices Past Due Date"
+ },
+ {
+ "fieldname": "end_date",
+ "fieldtype": "Date",
+ "label": "Subscription End Date",
+ "set_only_once": 1
+ },
+ {
+ "fieldname": "start_date",
+ "fieldtype": "Date",
+ "label": "Subscription Start Date",
+ "set_only_once": 1
+ },
+ {
+ "fieldname": "cost_center",
+ "fieldtype": "Link",
+ "label": "Cost Center",
+ "options": "Cost Center"
}
],
"links": [],
- "modified": "2020-01-27 14:37:32.845173",
+ "modified": "2020-06-25 10:52:52.265105",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Subscription",
diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py
index 0933c7e..0752531 100644
--- a/erpnext/accounts/doctype/subscription/subscription.py
+++ b/erpnext/accounts/doctype/subscription/subscription.py
@@ -7,7 +7,7 @@
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
+from frappe.utils.data import nowdate, getdate, cstr, cint, add_days, date_diff, get_last_day, add_to_date, flt
from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions
@@ -15,7 +15,7 @@
class Subscription(Document):
def before_insert(self):
# update start just before the subscription doc is created
- self.update_subscription_period(self.start)
+ self.update_subscription_period(self.start_date)
def update_subscription_period(self, date=None):
"""
@@ -35,7 +35,9 @@
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():
+ if self.is_new_subscription() and self.trial_period_end and getdate(self.trial_period_end) > getdate(self.start_date):
+ self.current_invoice_start = add_days(self.trial_period_end, 1)
+ elif self.trial_period_start and self.is_trialling():
self.current_invoice_start = self.trial_period_start
elif date:
self.current_invoice_start = date
@@ -53,15 +55,45 @@
current billing period where `x` is the billing interval from the
`Subscription Plan` in the `Subscription`.
"""
- if self.is_trialling():
+ if self.is_trialling() and getdate(self.current_invoice_start) < getdate(self.trial_period_end):
self.current_invoice_end = self.trial_period_end
else:
billing_cycle_info = self.get_billing_cycle_data()
if billing_cycle_info:
- self.current_invoice_end = add_to_date(self.current_invoice_start, **billing_cycle_info)
+ if self.is_new_subscription() and getdate(self.start_date) < getdate(self.current_invoice_start):
+ self.current_invoice_end = add_to_date(self.start_date, **billing_cycle_info)
+
+ # For cases where trial period is for an entire billing interval
+ if getdate(self.current_invoice_end) < getdate(self.current_invoice_start):
+ self.current_invoice_end = add_to_date(self.current_invoice_start, **billing_cycle_info)
+ else:
+ 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)
+ if self.follow_calendar_months:
+ billing_info = self.get_billing_cycle_and_interval()
+ billing_interval_count = billing_info[0]['billing_interval_count']
+ calendar_months = get_calendar_months(billing_interval_count)
+ calendar_month = 0
+ current_invoice_end_month = getdate(self.current_invoice_end).month
+ current_invoice_end_year = getdate(self.current_invoice_end).year
+
+ for month in calendar_months:
+ if month <= current_invoice_end_month:
+ calendar_month = month
+
+ if cint(calendar_month - billing_interval_count) <= 0 and \
+ getdate(self.current_invoice_start).month != 1:
+ calendar_month = 12
+ current_invoice_end_year -= 1
+
+ self.current_invoice_end = get_last_day(cstr(current_invoice_end_year) + '-' \
+ + cstr(calendar_month) + '-01')
+
+ if self.end_date and getdate(self.current_invoice_end) > getdate(self.end_date):
+ self.current_invoice_end = self.end_date
+
@staticmethod
def validate_plans_billing_cycle(billing_cycle_data):
"""
@@ -132,21 +164,22 @@
"""
if self.is_trialling():
self.status = 'Trialling'
- elif self.status == 'Past Due Date' and self.is_past_grace_period():
+ elif self.status == 'Active' and self.end_date and getdate() > getdate(self.end_date):
+ self.status = 'Completed'
+ elif self.is_past_grace_period():
subscription_settings = frappe.get_single('Subscription Settings')
self.status = 'Cancelled' if cint(subscription_settings.cancel_after_grace) else 'Unpaid'
- elif self.status == 'Past Due Date' and not self.has_outstanding_invoice():
- self.status = 'Active'
- elif self.current_invoice_is_past_due():
+ elif self.current_invoice_is_past_due() and not self.is_past_grace_period():
self.status = 'Past Due Date'
+ elif not self.has_outstanding_invoice():
+ self.status = 'Active'
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.
+ Returns `True` if the `Subscription` is in trial period.
"""
return not self.period_has_passed(self.trial_period_end) and self.is_new_subscription()
@@ -160,7 +193,7 @@
return True
end_date = getdate(end_date)
- return getdate(nowdate()) > getdate(end_date)
+ return getdate() > getdate(end_date)
def is_past_grace_period(self):
"""
@@ -171,7 +204,7 @@
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)
+ return getdate() > add_days(current_invoice.due_date, grace_period)
def current_invoice_is_past_due(self, current_invoice=None):
"""
@@ -180,22 +213,24 @@
if not current_invoice:
current_invoice = self.get_current_invoice()
- if not current_invoice:
+ if not current_invoice or self.is_paid(current_invoice):
return False
else:
- return getdate(nowdate()) > getdate(current_invoice.due_date)
+ return getdate() > getdate(current_invoice.due_date)
def get_current_invoice(self):
"""
Returns the most recent generated invoice.
"""
+ doctype = 'Sales Invoice' if self.party_type == 'Customer' else 'Purchase Invoice'
+
if len(self.invoices):
current = self.invoices[-1]
- if frappe.db.exists('Sales Invoice', current.invoice):
- doc = frappe.get_doc('Sales Invoice', current.invoice)
+ if frappe.db.exists(doctype, current.get('invoice')):
+ doc = frappe.get_doc(doctype, current.get('invoice'))
return doc
else:
- frappe.throw(_('Invoice {0} no longer exists').format(current.invoice))
+ frappe.throw(_('Invoice {0} no longer exists').format(current.get('invoice')))
def is_new_subscription(self):
"""
@@ -206,6 +241,8 @@
def validate(self):
self.validate_trial_period()
self.validate_plans_billing_cycle(self.get_billing_cycle_and_interval())
+ self.validate_end_date()
+ self.validate_to_follow_calendar_months()
def validate_trial_period(self):
"""
@@ -215,34 +252,72 @@
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:
+ if self.trial_period_start and not self.trial_period_end:
frappe.throw(_('Both Trial Period Start Date and Trial Period End Date must be set'))
+ if self.trial_period_start and getdate(self.trial_period_start) > getdate(self.start_date):
+ frappe.throw(_('Trial Period Start date cannot be after Subscription Start Date'))
+
+ def validate_end_date(self):
+ billing_cycle_info = self.get_billing_cycle_data()
+ end_date = add_to_date(self.start_date, **billing_cycle_info)
+
+ if self.end_date and getdate(self.end_date) <= getdate(end_date):
+ frappe.throw(_('Subscription End Date must be after {0} as per the subscription plan').format(end_date))
+
+ def validate_to_follow_calendar_months(self):
+ if self.follow_calendar_months:
+ billing_info = self.get_billing_cycle_and_interval()
+
+ if not self.end_date:
+ frappe.throw(_('Subscription End Date is mandatory to follow calendar months'))
+
+ if billing_info[0]['billing_interval'] != 'Month':
+ frappe.throw('Billing Interval in Subscription Plan must be Month to follow calendar months')
+
def after_insert(self):
# 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
+ Creates a `Invoice` for the `Subscription`, updates `self.invoices` and
saves the `Subscription`.
"""
+
+ doctype = 'Sales Invoice' if self.party_type == 'Customer' else 'Purchase Invoice'
+
invoice = self.create_invoice(prorate)
- self.append('invoices', {'invoice': invoice.name})
+ self.append('invoices', {
+ 'document_type': doctype,
+ 'invoice': invoice.name
+ })
+
self.save()
return invoice
def create_invoice(self, prorate):
"""
- Creates a `Sales Invoice`, submits it and returns it
+ Creates a `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.customer
+ doctype = 'Sales Invoice' if self.party_type == 'Customer' else 'Purchase Invoice'
- ## Add dimesnions in invoice for subscription:
+ invoice = frappe.new_doc(doctype)
+ invoice.set_posting_time = 1
+ invoice.posting_date = self.current_invoice_start if self.generate_invoice_at_period_start \
+ else self.current_invoice_end
+
+ invoice.cost_center = self.cost_center
+
+ if doctype == 'Sales Invoice':
+ invoice.customer = self.party
+ else:
+ invoice.supplier = self.party
+ if frappe.db.get_value('Supplier', self.party, 'tax_withholding_category'):
+ invoice.apply_tds = 1
+
+ ## Add dimensions in invoice for subscription:
accounting_dimensions = get_accounting_dimensions()
for dimension in accounting_dimensions:
@@ -255,18 +330,25 @@
# for that reason
items_list = self.get_items_from_plans(self.plans, prorate)
for item in items_list:
- invoice.append('items', item)
+ invoice.append('items', item)
# Taxes
- if self.tax_template:
- invoice.taxes_and_charges = self.tax_template
+ tax_template = ''
+
+ if doctype == 'Sales Invoice' and self.sales_tax_template:
+ tax_template = self.sales_tax_template
+ if doctype == 'Purchase Invoice' and self.purchase_tax_template:
+ tax_template = self.purchase_tax_template
+
+ if tax_template:
+ invoice.taxes_and_charges = tax_template
invoice.set_taxes()
# Due date
invoice.append(
'payment_schedule',
{
- 'due_date': add_days(self.current_invoice_end, cint(self.days_until_due)),
+ 'due_date': add_days(invoice.posting_date, cint(self.days_until_due)),
'invoice_portion': 100
}
)
@@ -300,13 +382,42 @@
prorate_factor = get_prorata_factor(self.current_invoice_end, self.current_invoice_start)
items = []
- customer = self.customer
+ party = self.party
for plan in plans:
- item_code = frappe.db.get_value("Subscription Plan", plan.plan, "item")
- if not prorate:
- items.append({'item_code': item_code, 'qty': plan.qty, 'rate': get_plan_rate(plan.plan, plan.qty, customer)})
+ plan_doc = frappe.get_doc('Subscription Plan', plan.plan)
+
+ item_code = plan_doc.item
+
+ if self.party == 'Customer':
+ deferred_field = 'enable_deferred_revenue'
else:
- items.append({'item_code': item_code, 'qty': plan.qty, 'rate': (get_plan_rate(plan.plan, plan.qty, customer) * prorate_factor)})
+ deferred_field = 'enable_deferred_expense'
+
+ deferred = frappe.db.get_value('Item', item_code, deferred_field)
+
+ if not prorate:
+ item = {'item_code': item_code, 'qty': plan.qty, 'rate': get_plan_rate(plan.plan, plan.qty, party,
+ self.current_invoice_start, self.current_invoice_end), 'cost_center': plan_doc.cost_center}
+ else:
+ item = {'item_code': item_code, 'qty': plan.qty, 'rate': get_plan_rate(plan.plan, plan.qty, party,
+ self.current_invoice_start, self.current_invoice_end, prorate_factor), 'cost_center': plan_doc.cost_center}
+
+ if deferred:
+ item.update({
+ deferred_field: deferred,
+ 'service_start_date': self.current_invoice_start,
+ 'service_end_date': self.current_invoice_end
+ })
+
+ accounting_dimensions = get_accounting_dimensions()
+
+ for dimension in accounting_dimensions:
+ if plan_doc.get(dimension):
+ item.update({
+ dimension: plan_doc.get(dimension)
+ })
+
+ items.append(item)
return items
@@ -322,12 +433,13 @@
elif self.status in ['Past Due Date', 'Unpaid']:
self.process_for_past_due_date()
+ self.set_subscription_status()
+
self.save()
def is_postpaid_to_invoice(self):
- return getdate(nowdate()) > getdate(self.current_invoice_end) or \
- (getdate(nowdate()) >= getdate(self.current_invoice_end) and getdate(self.current_invoice_end) == getdate(self.current_invoice_start)) and \
- not self.has_outstanding_invoice()
+ return getdate() > getdate(self.current_invoice_end) or \
+ (getdate() >= getdate(self.current_invoice_end) and getdate(self.current_invoice_end) == getdate(self.current_invoice_start))
def is_prepaid_to_invoice(self):
if not self.generate_invoice_at_period_start:
@@ -337,14 +449,12 @@
return True
# Check invoice dates and make sure it doesn't have outstanding invoices
- return getdate(nowdate()) >= getdate(self.current_invoice_start) and not self.has_outstanding_invoice()
+ return getdate() >= getdate(self.current_invoice_start)
- def is_current_invoice_paid(self):
- if self.is_new_subscription():
- return False
+ def is_current_invoice_generated(self):
+ invoice = self.get_current_invoice()
- last_invoice = frappe.get_doc('Sales Invoice', self.invoices[-1].invoice)
- if getdate(last_invoice.posting_date) == getdate(self.current_invoice_start) and last_invoice.status == 'Paid':
+ if invoice and getdate(self.current_invoice_start) <= getdate(invoice.posting_date) <= getdate(self.current_invoice_end):
return True
return False
@@ -358,21 +468,23 @@
2. Change the `Subscription` status to 'Past Due Date'
3. Change the `Subscription` status to 'Cancelled'
"""
- if not self.is_current_invoice_paid() and (self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice()):
- self.generate_invoice()
- if self.current_invoice_is_past_due():
- self.status = 'Past Due Date'
+ if getdate() > getdate(self.current_invoice_end) and self.is_prepaid_to_invoice():
+ self.update_subscription_period(add_days(self.current_invoice_end, 1))
- if self.current_invoice_is_past_due() and getdate(nowdate()) > getdate(self.current_invoice_end):
- self.status = 'Past Due Date'
+ if not self.is_current_invoice_generated() and (self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice()):
+ prorate = frappe.db.get_single_value('Subscription Settings', 'prorate')
+ self.generate_invoice(prorate)
- if self.cancel_at_period_end and getdate(nowdate()) > getdate(self.current_invoice_end):
+ if self.cancel_at_period_end and getdate() > getdate(self.current_invoice_end):
self.cancel_subscription_at_period_end()
def cancel_subscription_at_period_end(self):
"""
Called when `Subscription.cancel_at_period_end` is truthy
"""
+ if self.end_date and getdate() < getdate(self.end_date):
+ return
+
self.status = 'Cancelled'
if not self.cancelation_date:
self.cancelation_date = nowdate()
@@ -390,14 +502,22 @@
if not current_invoice:
frappe.throw(_('Current invoice {0} is missing').format(current_invoice.invoice))
else:
- if self.is_not_outstanding(current_invoice):
+ if not self.has_outstanding_invoice():
self.status = 'Active'
- self.update_subscription_period(add_days(self.current_invoice_end, 1))
else:
self.set_status_grace_period()
+ if getdate() > getdate(self.current_invoice_end):
+ self.update_subscription_period(add_days(self.current_invoice_end, 1))
+
+ # Generate invoices periodically even if current invoice are unpaid
+ if self.generate_new_invoices_past_due_date and not self.is_current_invoice_generated() and (self.is_postpaid_to_invoice()
+ or self.is_prepaid_to_invoice()):
+ prorate = frappe.db.get_single_value('Subscription Settings', 'prorate')
+ self.generate_invoice(prorate)
+
@staticmethod
- def is_not_outstanding(invoice):
+ def is_paid(invoice):
"""
Return `True` if the given invoice is paid
"""
@@ -407,11 +527,17 @@
"""
Returns `True` if the most recent invoice for the `Subscription` is not paid
"""
+ doctype = 'Sales Invoice' if self.party_type == 'Customer' else 'Purchase Invoice'
current_invoice = self.get_current_invoice()
- if not current_invoice:
- return False
+ invoice_list = [d.invoice for d in self.invoices]
+
+ outstanding_invoices = frappe.get_all(doctype, fields=['name'],
+ filters={'status': ('!=', 'Paid'), 'name': ('in', invoice_list)})
+
+ if outstanding_invoices:
+ return True
else:
- return not self.is_not_outstanding(current_invoice)
+ False
def cancel_subscription(self):
"""
@@ -419,7 +545,7 @@
but it will not affect already created invoices.
"""
if self.status != 'Cancelled':
- to_generate_invoice = True if self.status == 'Active' else False
+ to_generate_invoice = True if self.status == 'Active' and not self.generate_invoice_at_period_start else False
to_prorate = frappe.db.get_single_value('Subscription Settings', 'prorate')
self.status = 'Cancelled'
self.cancelation_date = nowdate()
@@ -435,7 +561,7 @@
"""
if self.status == 'Cancelled':
self.status = 'Active'
- self.db_set('start', nowdate())
+ self.db_set('start_date', nowdate())
self.update_subscription_period(nowdate())
self.invoices = []
self.save()
@@ -447,6 +573,14 @@
if invoice:
return invoice.precision('grand_total')
+def get_calendar_months(billing_interval):
+ calendar_months = []
+ start = 0
+ while start < 12:
+ start += billing_interval
+ calendar_months.append(start)
+
+ return calendar_months
def get_prorata_factor(period_end, period_start):
diff = flt(date_diff(nowdate(), period_start) + 1)
@@ -469,10 +603,7 @@
"""
Returns all `Subscription` documents
"""
- return frappe.db.sql(
- 'select name from `tabSubscription` where status != "Cancelled"',
- as_dict=1
- )
+ return frappe.db.get_all('Subscription', {'status': ('!=','Cancelled')})
def process(data):
diff --git a/erpnext/accounts/doctype/subscription/subscription_list.js b/erpnext/accounts/doctype/subscription/subscription_list.js
index abcfc5e..a4edb77 100644
--- a/erpnext/accounts/doctype/subscription/subscription_list.js
+++ b/erpnext/accounts/doctype/subscription/subscription_list.js
@@ -4,6 +4,8 @@
return [__("Trialling"), "green"];
} else if(doc.status === 'Active') {
return [__("Active"), "green"];
+ } else if(doc.status === 'Completed') {
+ return [__("Completed"), "green"];
} else if(doc.status === 'Past Due Date') {
return [__("Past Due Date"), "orange"];
} else if(doc.status === 'Unpaid') {
diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py
index 3d96f23..f41f08a 100644
--- a/erpnext/accounts/doctype/subscription/test_subscription.py
+++ b/erpnext/accounts/doctype/subscription/test_subscription.py
@@ -7,7 +7,7 @@
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
+from frappe.utils.data import nowdate, add_days, add_to_date, add_months, date_diff, flt, get_date_str
def create_plan():
@@ -15,7 +15,7 @@
plan = frappe.new_doc('Subscription Plan')
plan.plan_name = '_Test Plan Name'
plan.item = '_Test Non Stock Item'
- plan.price_determination = "Fixed rate"
+ plan.price_determination = "Fixed Rate"
plan.cost = 900
plan.billing_interval = 'Month'
plan.billing_interval_count = 1
@@ -25,7 +25,7 @@
plan = frappe.new_doc('Subscription Plan')
plan.plan_name = '_Test Plan Name 2'
plan.item = '_Test Non Stock Item'
- plan.price_determination = "Fixed rate"
+ plan.price_determination = "Fixed Rate"
plan.cost = 1999
plan.billing_interval = 'Month'
plan.billing_interval_count = 1
@@ -35,12 +35,29 @@
plan = frappe.new_doc('Subscription Plan')
plan.plan_name = '_Test Plan Name 3'
plan.item = '_Test Non Stock Item'
- plan.price_determination = "Fixed rate"
+ plan.price_determination = "Fixed Rate"
plan.cost = 1999
plan.billing_interval = 'Day'
plan.billing_interval_count = 14
plan.insert()
+ # Defined a quarterly Subscription Plan
+ if not frappe.db.exists('Subscription Plan', '_Test Plan Name 4'):
+ plan = frappe.new_doc('Subscription Plan')
+ plan.plan_name = '_Test Plan Name 4'
+ plan.item = '_Test Non Stock Item'
+ plan.price_determination = "Monthly Rate"
+ plan.cost = 20000
+ plan.billing_interval = 'Month'
+ plan.billing_interval_count = 3
+ plan.insert()
+
+ if not frappe.db.exists('Supplier', '_Test Supplier'):
+ supplier = frappe.new_doc('Supplier')
+ supplier.supplier_name = '_Test Supplier'
+ supplier.supplier_group = 'All Supplier Groups'
+ supplier.insert()
+
class TestSubscription(unittest.TestCase):
def setUp(self):
@@ -48,7 +65,8 @@
def test_create_subscription_with_trial_with_correct_period(self):
subscription = frappe.new_doc('Subscription')
- subscription.customer = '_Test Customer'
+ subscription.party_type = 'Customer'
+ subscription.party = '_Test Customer'
subscription.trial_period_start = nowdate()
subscription.trial_period_end = add_days(nowdate(), 30)
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
@@ -56,8 +74,8 @@
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(add_days(subscription.trial_period_end, 1), get_date_str(subscription.current_invoice_start))
+ self.assertEqual(add_days(subscription.current_invoice_start, 30), get_date_str(subscription.current_invoice_end))
self.assertEqual(subscription.invoices, [])
self.assertEqual(subscription.status, 'Trialling')
@@ -65,7 +83,8 @@
def test_create_subscription_without_trial_with_correct_period(self):
subscription = frappe.new_doc('Subscription')
- subscription.customer = '_Test Customer'
+ subscription.party_type = 'Customer'
+ subscription.party = '_Test Customer'
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
subscription.save()
@@ -81,7 +100,8 @@
def test_create_subscription_trial_with_wrong_dates(self):
subscription = frappe.new_doc('Subscription')
- subscription.customer = '_Test Customer'
+ subscription.party_type = 'Customer'
+ subscription.party = '_Test Customer'
subscription.trial_period_end = nowdate()
subscription.trial_period_start = add_days(nowdate(), 30)
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
@@ -91,7 +111,8 @@
def test_create_subscription_multi_with_different_billing_fails(self):
subscription = frappe.new_doc('Subscription')
- subscription.customer = '_Test Customer'
+ subscription.party_type = 'Customer'
+ subscription.party = '_Test Customer'
subscription.trial_period_end = nowdate()
subscription.trial_period_start = add_days(nowdate(), 30)
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
@@ -102,8 +123,9 @@
def test_invoice_is_generated_at_end_of_billing_period(self):
subscription = frappe.new_doc('Subscription')
- subscription.customer = '_Test Customer'
- subscription.start = '2018-01-01'
+ subscription.party_type = 'Customer'
+ subscription.party = '_Test Customer'
+ subscription.start_date = '2018-01-01'
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
subscription.insert()
@@ -114,18 +136,22 @@
self.assertEqual(len(subscription.invoices), 1)
self.assertEqual(subscription.current_invoice_start, '2018-01-01')
- self.assertEqual(subscription.status, 'Past Due Date')
+ subscription.process()
+ self.assertEqual(subscription.status, 'Unpaid')
subscription.delete()
def test_status_goes_back_to_active_after_invoice_is_paid(self):
subscription = frappe.new_doc('Subscription')
- subscription.customer = '_Test Customer'
+ subscription.party_type = 'Customer'
+ subscription.party = '_Test Customer'
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
- subscription.start = '2018-01-01'
+ subscription.start_date = '2018-01-01'
subscription.insert()
subscription.process() # generate first invoice
self.assertEqual(len(subscription.invoices), 1)
- self.assertEqual(subscription.status, 'Past Due Date')
+
+ # Status is unpaid as Days until Due is zero and grace period is Zero
+ self.assertEqual(subscription.status, 'Unpaid')
subscription.get_current_invoice()
current_invoice = subscription.get_current_invoice()
@@ -137,7 +163,7 @@
subscription.process()
self.assertEqual(subscription.status, 'Active')
- self.assertEqual(subscription.current_invoice_start, add_months(subscription.start, 1))
+ self.assertEqual(subscription.current_invoice_start, add_months(subscription.start_date, 1))
self.assertEqual(len(subscription.invoices), 1)
subscription.delete()
@@ -149,16 +175,17 @@
settings.save()
subscription = frappe.new_doc('Subscription')
- subscription.customer = '_Test Customer'
+ subscription.party_type = 'Customer'
+ subscription.party = '_Test Customer'
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
- subscription.start = '2018-01-01'
+ subscription.start_date = '2018-01-01'
subscription.insert()
+
+ self.assertEqual(subscription.status, 'Active')
+
subscription.process() # generate first invoice
-
- self.assertEqual(subscription.status, 'Past Due Date')
-
- subscription.process()
# This should change status to Cancelled since grace period is 0
+ # And is backdated subscription so subscription will be cancelled after processing
self.assertEqual(subscription.status, 'Cancelled')
settings.cancel_after_grace = default_grace_period_action
@@ -172,16 +199,14 @@
settings.save()
subscription = frappe.new_doc('Subscription')
- subscription.customer = '_Test Customer'
+ subscription.party_type = 'Customer'
+ subscription.party = '_Test Customer'
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
- subscription.start = '2018-01-01'
+ subscription.start_date = '2018-01-01'
subscription.insert()
subscription.process() # generate first invoice
- self.assertEqual(subscription.status, 'Past Due Date')
-
- subscription.process()
- # This should change status to Cancelled since grace period is 0
+ # Status is unpaid as Days until Due is zero and grace period is Zero
self.assertEqual(subscription.status, 'Unpaid')
settings.cancel_after_grace = default_grace_period_action
@@ -190,10 +215,11 @@
def test_subscription_invoice_days_until_due(self):
subscription = frappe.new_doc('Subscription')
- subscription.customer = '_Test Customer'
+ subscription.party_type = 'Customer'
+ subscription.party = '_Test Customer'
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
subscription.days_until_due = 10
- subscription.start = add_months(nowdate(), -1)
+ subscription.start_date = add_months(nowdate(), -1)
subscription.insert()
subscription.process() # generate first invoice
self.assertEqual(len(subscription.invoices), 1)
@@ -208,9 +234,10 @@
settings.save()
subscription = frappe.new_doc('Subscription')
- subscription.customer = '_Test Customer'
+ subscription.party_type = 'Customer'
+ subscription.party = '_Test Customer'
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
- subscription.start = '2018-01-01'
+ subscription.start_date = '2018-01-01'
subscription.insert()
subscription.process() # generate first invoice
@@ -232,7 +259,8 @@
def test_subscription_remains_active_during_invoice_period(self):
subscription = frappe.new_doc('Subscription')
- subscription.customer = '_Test Customer'
+ subscription.party_type = 'Customer'
+ subscription.party = '_Test Customer'
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
subscription.save()
subscription.process() # no changes expected
@@ -258,7 +286,8 @@
def test_subscription_cancelation(self):
subscription = frappe.new_doc('Subscription')
- subscription.customer = '_Test Customer'
+ subscription.party_type = 'Customer'
+ subscription.party = '_Test Customer'
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
subscription.save()
subscription.cancel_subscription()
@@ -274,7 +303,8 @@
settings.save()
subscription = frappe.new_doc('Subscription')
- subscription.customer = '_Test Customer'
+ subscription.party_type = 'Customer'
+ subscription.party = '_Test Customer'
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
subscription.save()
@@ -309,7 +339,8 @@
settings.save()
subscription = frappe.new_doc('Subscription')
- subscription.customer = '_Test Customer'
+ subscription.party_type = 'Customer'
+ subscription.party = '_Test Customer'
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
subscription.save()
subscription.cancel_subscription()
@@ -329,7 +360,8 @@
settings.save()
subscription = frappe.new_doc('Subscription')
- subscription.customer = '_Test Customer'
+ subscription.party_type = 'Customer'
+ subscription.party = '_Test Customer'
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
subscription.save()
subscription.cancel_subscription()
@@ -353,16 +385,14 @@
settings.save()
subscription = frappe.new_doc('Subscription')
- subscription.customer = '_Test Customer'
+ subscription.party_type = 'Customer'
+ subscription.party = '_Test Customer'
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
- subscription.start = '2018-01-01'
+ subscription.start_date = '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, 'Cancelled')
self.assertEqual(len(subscription.invoices), invoices)
@@ -387,15 +417,14 @@
settings.save()
subscription = frappe.new_doc('Subscription')
- subscription.customer = '_Test Customer'
+ subscription.party_type = 'Customer'
+ subscription.party = '_Test Customer'
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
- subscription.start = '2018-01-01'
+ subscription.start_date = '2018-01-01'
subscription.insert()
subscription.process() # generate first invoice
- self.assertEqual(subscription.status, 'Past Due Date')
-
- subscription.process()
+ # Status is unpaid as Days until Due is zero and grace period is Zero
self.assertEqual(subscription.status, 'Unpaid')
subscription.cancel_subscription()
@@ -424,16 +453,14 @@
settings.save()
subscription = frappe.new_doc('Subscription')
- subscription.customer = '_Test Customer'
+ subscription.party_type = 'Customer'
+ subscription.party = '_Test Customer'
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
- subscription.start = '2018-01-01'
+ subscription.start_date = '2018-01-01'
subscription.insert()
+
subscription.process() # generate first invoice
-
- self.assertEqual(subscription.status, 'Past Due Date')
-
- subscription.process()
- # This should change status to Cancelled since grace period is 0
+ # This should change status to Unpaid since grace period is 0
self.assertEqual(subscription.status, 'Unpaid')
invoice = subscription.get_current_invoice()
@@ -445,7 +472,7 @@
# A new invoice is generated
subscription.process()
- self.assertEqual(subscription.status, 'Past Due Date')
+ self.assertEqual(subscription.status, 'Unpaid')
settings.cancel_after_grace = default_grace_period_action
settings.save()
@@ -453,7 +480,8 @@
def test_restart_active_subscription(self):
subscription = frappe.new_doc('Subscription')
- subscription.customer = '_Test Customer'
+ subscription.party_type = 'Customer'
+ subscription.party = '_Test Customer'
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
subscription.save()
@@ -463,7 +491,8 @@
def test_subscription_invoice_discount_percentage(self):
subscription = frappe.new_doc('Subscription')
- subscription.customer = '_Test Customer'
+ subscription.party_type = 'Customer'
+ subscription.party = '_Test Customer'
subscription.additional_discount_percentage = 10
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
subscription.save()
@@ -478,7 +507,8 @@
def test_subscription_invoice_discount_amount(self):
subscription = frappe.new_doc('Subscription')
- subscription.customer = '_Test Customer'
+ subscription.party_type = 'Customer'
+ subscription.party = '_Test Customer'
subscription.additional_discount_amount = 11
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
subscription.save()
@@ -495,7 +525,8 @@
# Create a non pre-billed subscription, processing should not create
# invoices.
subscription = frappe.new_doc('Subscription')
- subscription.customer = '_Test Customer'
+ subscription.party_type = 'Customer'
+ subscription.party = '_Test Customer'
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
subscription.save()
subscription.process()
@@ -517,10 +548,12 @@
settings.save()
subscription = frappe.new_doc('Subscription')
- subscription.customer = '_Test Customer'
+ subscription.party_type = 'Customer'
+ subscription.party = '_Test Customer'
subscription.generate_invoice_at_period_start = True
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
subscription.save()
+ subscription.process()
subscription.cancel_subscription()
self.assertEqual(len(subscription.invoices), 1)
@@ -538,3 +571,65 @@
settings.save()
subscription.delete()
+
+ def test_subscription_with_follow_calendar_months(self):
+ subscription = frappe.new_doc('Subscription')
+ subscription.party_type = 'Supplier'
+ subscription.party = '_Test Supplier'
+ subscription.generate_invoice_at_period_start = 1
+ subscription.follow_calendar_months = 1
+
+ # select subscription start date as '2018-01-15'
+ subscription.start_date = '2018-01-15'
+ subscription.end_date = '2018-07-15'
+ subscription.append('plans', {'plan': '_Test Plan Name 4', 'qty': 1})
+ subscription.save()
+
+ # even though subscription starts at '2018-01-15' and Billing interval is Month and count 3
+ # First invoice will end at '2018-03-31' instead of '2018-04-14'
+ self.assertEqual(get_date_str(subscription.current_invoice_end), '2018-03-31')
+
+ def test_subscription_generate_invoice_past_due(self):
+ subscription = frappe.new_doc('Subscription')
+ subscription.party_type = 'Supplier'
+ subscription.party = '_Test Supplier'
+ subscription.generate_invoice_at_period_start = 1
+ subscription.generate_new_invoices_past_due_date = 1
+ # select subscription start date as '2018-01-15'
+ subscription.start_date = '2018-01-01'
+ subscription.append('plans', {'plan': '_Test Plan Name 4', 'qty': 1})
+ subscription.save()
+
+ # Process subscription and create first invoice
+ # Subscription status will be unpaid since due date has already passed
+ subscription.process()
+ self.assertEqual(len(subscription.invoices), 1)
+ self.assertEqual(subscription.status, 'Unpaid')
+
+ # Now the Subscription is unpaid
+ # Even then new invoice should be created as we have enabled `generate_new_invoices_past_due_date` in
+ # subscription
+
+ subscription.process()
+ self.assertEqual(len(subscription.invoices), 2)
+
+ def test_subscription_without_generate_invoice_past_due(self):
+ subscription = frappe.new_doc('Subscription')
+ subscription.party_type = 'Supplier'
+ subscription.party = '_Test Supplier'
+ subscription.generate_invoice_at_period_start = 1
+ # select subscription start date as '2018-01-15'
+ subscription.start_date = '2018-01-01'
+ subscription.append('plans', {'plan': '_Test Plan Name 4', 'qty': 1})
+ subscription.save()
+
+ # Process subscription and create first invoice
+ # Subscription status will be unpaid since due date has already passed
+ subscription.process()
+ self.assertEqual(len(subscription.invoices), 1)
+ self.assertEqual(subscription.status, 'Unpaid')
+
+ subscription.process()
+ self.assertEqual(len(subscription.invoices), 1)
+
+
diff --git a/erpnext/accounts/doctype/subscription_invoice/subscription_invoice.json b/erpnext/accounts/doctype/subscription_invoice/subscription_invoice.json
index c4bae1d..f54e887 100644
--- a/erpnext/accounts/doctype/subscription_invoice/subscription_invoice.json
+++ b/erpnext/accounts/doctype/subscription_invoice/subscription_invoice.json
@@ -1,73 +1,40 @@
{
- "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",
+ "actions": [],
+ "creation": "2018-02-26 04:21:41.265055",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "document_type",
+ "invoice"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "invoice",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Invoice",
- "length": 0,
- "no_copy": 0,
- "options": "Sales Invoice",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldname": "document_type",
+ "fieldtype": "Link",
+ "label": "Document Type ",
+ "options": "DocType",
+ "read_only": 1
+ },
+ {
+ "fieldname": "invoice",
+ "fieldtype": "Dynamic Link",
+ "in_list_view": 1,
+ "label": "Invoice",
+ "options": "document_type",
+ "read_only": 1
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "modified": "2018-02-26 10:48:07.033422",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Subscription Invoice",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2020-06-01 22:23:54.462718",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Subscription Invoice",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/subscription_plan/subscription_plan.json b/erpnext/accounts/doctype/subscription_plan/subscription_plan.json
index 9f79066..46ce093 100644
--- a/erpnext/accounts/doctype/subscription_plan/subscription_plan.json
+++ b/erpnext/accounts/doctype/subscription_plan/subscription_plan.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"allow_rename": 1,
"autoname": "field:plan_name",
"creation": "2018-02-24 11:31:23.066506",
@@ -24,6 +25,7 @@
"column_break_16",
"payment_gateway",
"accounting_dimensions_section",
+ "cost_center",
"dimension_col_break"
],
"fields": [
@@ -60,8 +62,8 @@
{
"fieldname": "price_determination",
"fieldtype": "Select",
- "label": "Price Determination",
- "options": "\nFixed rate\nBased on price list",
+ "label": "Subscription Price Based On",
+ "options": "\nFixed Rate\nBased On Price List\nMonthly Rate",
"reqd": 1
},
{
@@ -69,7 +71,7 @@
"fieldtype": "Column Break"
},
{
- "depends_on": "eval:doc.price_determination==\"Fixed rate\"",
+ "depends_on": "eval:['Fixed Rate', 'Monthly Rate'].includes(doc.price_determination)",
"fieldname": "cost",
"fieldtype": "Currency",
"in_list_view": 1,
@@ -136,9 +138,16 @@
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "cost_center",
+ "fieldtype": "Link",
+ "label": "Cost Center",
+ "options": "Cost Center"
}
],
- "modified": "2019-07-25 18:35:04.362556",
+ "links": [],
+ "modified": "2020-06-25 10:53:44.205774",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Subscription Plan",
@@ -155,6 +164,30 @@
"role": "System Manager",
"share": 1,
"write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts User",
+ "share": 1,
+ "write": 1
}
],
"sort_field": "modified",
diff --git a/erpnext/accounts/doctype/subscription_plan/subscription_plan.py b/erpnext/accounts/doctype/subscription_plan/subscription_plan.py
index 625979b..1ca442a 100644
--- a/erpnext/accounts/doctype/subscription_plan/subscription_plan.py
+++ b/erpnext/accounts/doctype/subscription_plan/subscription_plan.py
@@ -5,6 +5,7 @@
from __future__ import unicode_literals
import frappe
from frappe import _
+from frappe.utils import get_first_day, get_last_day, date_diff, flt, getdate
from frappe.model.document import Document
from erpnext.utilities.product import get_price
@@ -17,12 +18,12 @@
frappe.throw(_('Billing Interval Count cannot be less than 1'))
@frappe.whitelist()
-def get_plan_rate(plan, quantity=1, customer=None):
+def get_plan_rate(plan, quantity=1, customer=None, start_date=None, end_date=None, prorate_factor=1):
plan = frappe.get_doc("Subscription Plan", plan)
- if plan.price_determination == "Fixed rate":
- return plan.cost
+ if plan.price_determination == "Fixed Rate":
+ return plan.cost * prorate_factor
- elif plan.price_determination == "Based on price list":
+ elif plan.price_determination == "Based On Price List":
if customer:
customer_group = frappe.db.get_value("Customer", customer, "customer_group")
else:
@@ -32,4 +33,25 @@
if not price:
return 0
else:
- return price.price_list_rate
+ return price.price_list_rate * prorate_factor
+
+ elif plan.price_determination == 'Monthly Rate':
+ start_date = getdate(start_date)
+ end_date = getdate(end_date)
+
+ no_of_months = (end_date.year - start_date.year) * 12 + (end_date.month - start_date.month) + 1
+ cost = plan.cost * no_of_months
+
+ # Adjust cost if start or end date is not month start or end
+ prorate = frappe.db.get_single_value('Subscription Settings', 'prorate')
+
+ if prorate:
+ prorate_factor = flt(date_diff(start_date, get_first_day(start_date)) / date_diff(
+ get_last_day(start_date), get_first_day(start_date)), 1)
+
+ prorate_factor += flt(date_diff(get_last_day(end_date), end_date) / date_diff(
+ get_last_day(end_date), get_first_day(end_date)), 1)
+
+ cost -= (plan.cost * prorate_factor)
+
+ return cost
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/subscription_plan_detail/subscription_plan_detail.json b/erpnext/accounts/doctype/subscription_plan_detail/subscription_plan_detail.json
index ca54a16..3e16303 100644
--- a/erpnext/accounts/doctype/subscription_plan_detail/subscription_plan_detail.json
+++ b/erpnext/accounts/doctype/subscription_plan_detail/subscription_plan_detail.json
@@ -1,106 +1,40 @@
{
- "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",
+ "actions": [],
+ "creation": "2018-02-25 07:35:07.736146",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "plan",
+ "qty"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "qty",
- "fieldtype": "Int",
- "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": "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": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "qty",
+ "fieldtype": "Int",
+ "in_list_view": 1,
+ "label": "Quantity",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 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
+ "fieldname": "plan",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Plan",
+ "options": "Subscription Plan",
+ "reqd": 1
}
- ],
- "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-06-20 15:35:13.514699",
- "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
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2020-06-14 17:44:05.275100",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Subscription Plan Detail",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/subscription_settings/subscription_settings.json b/erpnext/accounts/doctype/subscription_settings/subscription_settings.json
index 8c7c6f3..821db7e 100644
--- a/erpnext/accounts/doctype/subscription_settings/subscription_settings.json
+++ b/erpnext/accounts/doctype/subscription_settings/subscription_settings.json
@@ -1,179 +1,76 @@
{
- "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",
+ "actions": [],
+ "creation": "2018-02-26 06:13:37.910139",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "grace_period",
+ "cancel_after_grace",
+ "prorate"
+ ],
"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
- },
+ "default": "1",
+ "description": "Number of days after invoice date has elapsed before canceling subscription or marking subscription as unpaid",
+ "fieldname": "grace_period",
+ "fieldtype": "Int",
+ "label": "Grace Period"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "0",
- "fieldname": "cancel_after_grace",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Cancel Invoice After Grace Period",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "default": "0",
+ "fieldname": "cancel_after_grace",
+ "fieldtype": "Check",
+ "label": "Cancel Subscription After Grace Period"
+ },
{
- "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
+ "default": "1",
+ "fieldname": "prorate",
+ "fieldtype": "Check",
+ "label": "Prorate"
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 1,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-02-26 13:58:09.455832",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Subscription Settings",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "issingle": 1,
+ "links": [],
+ "modified": "2020-06-23 09:13:44.292792",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Subscription Settings",
+ "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,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "System Manager",
+ "share": 1,
"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,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "Accounts Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "Accounts User",
+ "share": 1,
"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
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index fb88fec..0c2b873 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -697,6 +697,7 @@
erpnext.patches.v12_0.update_uom_conversion_factor
erpnext.patches.v13_0.delete_old_purchase_reports
erpnext.patches.v12_0.set_italian_import_supplier_invoice_permissions
+erpnext.patches.v13_0.update_subscription
erpnext.patches.v12_0.unhide_cost_center_field
erpnext.patches.v13_0.update_sla_enhancements
erpnext.patches.v12_0.update_address_template_for_india
diff --git a/erpnext/patches/v13_0/update_subscription.py b/erpnext/patches/v13_0/update_subscription.py
new file mode 100644
index 0000000..871ebf1
--- /dev/null
+++ b/erpnext/patches/v13_0/update_subscription.py
@@ -0,0 +1,41 @@
+# Copyright (c) 2019, Frappe and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+from six import iteritems
+
+def execute():
+
+ frappe.reload_doc('accounts', 'doctype', 'subscription')
+ frappe.reload_doc('accounts', 'doctype', 'subscription_invoice')
+ frappe.reload_doc('accounts', 'doctype', 'subscription_plan')
+
+ if frappe.db.has_column('Subscription', 'customer'):
+ frappe.db.sql("""
+ UPDATE `tabSubscription`
+ SET
+ start_date = start,
+ party_type = 'Customer',
+ party = customer,
+ sales_tax_template = tax_template
+ WHERE IFNULL(party,'') = ''
+ """)
+
+ frappe.db.sql("""
+ UPDATE `tabSubscription Invoice`
+ SET document_type = 'Sales Invoice'
+ WHERE IFNULL(document_type, '') = ''
+ """)
+
+ price_determination_map = {
+ 'Fixed rate': 'Fixed Rate',
+ 'Based on price list': 'Based On Price List'
+ }
+
+ for key, value in iteritems(price_determination_map):
+ frappe.db.sql("""
+ UPDATE `tabSubscription Plan`
+ SET price_determination = %s
+ WHERE price_determination = %s
+ """, (value, key))
\ No newline at end of file