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
This commit is contained in:
Deepesh Garg 2020-07-23 11:11:23 +05:30 committed by GitHub
parent a18f2ec23e
commit 9c49f2d886
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 678 additions and 495 deletions

View File

@ -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'){

View File

@ -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,18 +37,10 @@
"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",
@ -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
},
{
@ -61,12 +58,6 @@
"fieldtype": "Section Break",
"label": "Subscription Period"
},
{
"fieldname": "start",
"fieldtype": "Date",
"label": "Subscription Start Date",
"set_only_once": 1
},
{
"fieldname": "cancelation_date",
"fieldtype": "Date",
@ -137,16 +128,11 @@
"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",
@ -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",

View File

@ -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, 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 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import g
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 @@ class Subscription(Document):
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 @@ class Subscription(Document):
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 @@ class Subscription(Document):
"""
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 @@ class Subscription(Document):
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 @@ class Subscription(Document):
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 @@ class Subscription(Document):
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 @@ class Subscription(Document):
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 @@ class Subscription(Document):
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 @@ class Subscription(Document):
# 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 @@ class Subscription(Document):
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 @@ class Subscription(Document):
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 @@ class Subscription(Document):
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 @@ class Subscription(Document):
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 @@ class Subscription(Document):
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 @@ class Subscription(Document):
"""
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 @@ class Subscription(Document):
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 @@ class Subscription(Document):
"""
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 @@ class Subscription(Document):
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 @@ def get_all_subscriptions():
"""
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):

View File

@ -4,6 +4,8 @@ frappe.listview_settings['Subscription'] = {
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') {

View File

@ -7,7 +7,7 @@ import unittest
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 @@ def create_plan():
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 @@ def create_plan():
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 @@ def create_plan():
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 @@ class TestSubscription(unittest.TestCase):
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 @@ class TestSubscription(unittest.TestCase):
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 @@ class TestSubscription(unittest.TestCase):
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 @@ class TestSubscription(unittest.TestCase):
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 @@ class TestSubscription(unittest.TestCase):
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 @@ class TestSubscription(unittest.TestCase):
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 @@ class TestSubscription(unittest.TestCase):
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 @@ class TestSubscription(unittest.TestCase):
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 @@ class TestSubscription(unittest.TestCase):
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 @@ class TestSubscription(unittest.TestCase):
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 @@ class TestSubscription(unittest.TestCase):
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 @@ class TestSubscription(unittest.TestCase):
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 @@ class TestSubscription(unittest.TestCase):
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 @@ class TestSubscription(unittest.TestCase):
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 @@ class TestSubscription(unittest.TestCase):
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 @@ class TestSubscription(unittest.TestCase):
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 @@ class TestSubscription(unittest.TestCase):
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 @@ class TestSubscription(unittest.TestCase):
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 @@ class TestSubscription(unittest.TestCase):
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 @@ class TestSubscription(unittest.TestCase):
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 @@ class TestSubscription(unittest.TestCase):
# 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 @@ class TestSubscription(unittest.TestCase):
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 @@ class TestSubscription(unittest.TestCase):
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 @@ class TestSubscription(unittest.TestCase):
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 @@ class TestSubscription(unittest.TestCase):
# 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 @@ class TestSubscription(unittest.TestCase):
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 @@ class TestSubscription(unittest.TestCase):
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)

View File

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

View File

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

View File

@ -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 @@ class SubscriptionPlan(Document):
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 @@ def get_plan_rate(plan, quantity=1, customer=None):
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

View File

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

View File

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

View File

@ -697,6 +697,7 @@ execute:frappe.rename_doc("Desk Page", "Loan Management", "Loan", force=True)
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

View File

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