diff --git a/erpnext/accounts/doctype/subscription/subscription.js b/erpnext/accounts/doctype/subscription/subscription.js index dcbec12f8ba..ba98eb9b2a2 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 32b97ba80b5..afb94fe9c95 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,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", diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py index 0933c7e8b84..07525317aab 100644 --- a/erpnext/accounts/doctype/subscription/subscription.py +++ b/erpnext/accounts/doctype/subscription/subscription.py @@ -7,7 +7,7 @@ from __future__ import unicode_literals import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils.data import nowdate, getdate, cint, add_days, date_diff, get_last_day, add_to_date, 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): diff --git a/erpnext/accounts/doctype/subscription/subscription_list.js b/erpnext/accounts/doctype/subscription/subscription_list.js index abcfc5e6962..a4edb77dc9e 100644 --- a/erpnext/accounts/doctype/subscription/subscription_list.js +++ b/erpnext/accounts/doctype/subscription/subscription_list.js @@ -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') { diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py index 3d96f233b40..f41f08a6c41 100644 --- a/erpnext/accounts/doctype/subscription/test_subscription.py +++ b/erpnext/accounts/doctype/subscription/test_subscription.py @@ -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) + + diff --git a/erpnext/accounts/doctype/subscription_invoice/subscription_invoice.json b/erpnext/accounts/doctype/subscription_invoice/subscription_invoice.json index c4bae1d3c30..f54e887f263 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 9f790662356..46ce0939e4f 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 625979bee1f..1ca442a4531 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 @@ 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 \ 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 ca54a167f59..3e1630342cb 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 8c7c6f34e5f..821db7e95cc 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 fb88fec1750..0c2b873f15b 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -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 diff --git a/erpnext/patches/v13_0/update_subscription.py b/erpnext/patches/v13_0/update_subscription.py new file mode 100644 index 00000000000..871ebf17c4e --- /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