Merge pull request #17046 from nabinhait/deferred_revenue

Refactor: deferred revenue booking logic
This commit is contained in:
Nabin Hait 2019-03-29 12:39:36 +05:30 committed by GitHub
commit cf7538c17f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 176 additions and 92 deletions

View File

@ -2,9 +2,9 @@ from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import date_diff, add_months, today, getdate, add_days, flt
from frappe.utils import date_diff, add_months, today, getdate, add_days, flt, get_last_day
from erpnext.accounts.utils import get_account_currency
from erpnext.accounts.general_ledger import make_gl_entries
from frappe.email import sendmail_to_system_managers
def validate_service_stop_date(doc):
''' Validates service_stop_date for Purchase Invoice and Sales Invoice '''
@ -33,47 +33,49 @@ def validate_service_stop_date(doc):
frappe.throw(_("Cannot change Service Stop Date for item in row {0}".format(item.idx)))
def convert_deferred_expense_to_expense(start_date=None, end_date=None):
# book the expense/income on the last day, but it will be trigger on the 1st of month at 12:00 AM
if not start_date:
start_date = add_months(today(), -1)
if not end_date:
end_date = add_days(today(), -1)
# check for the purchase invoice for which GL entries has to be done
invoices = frappe.db.sql_list('''
select distinct parent from `tabPurchase Invoice Item` where service_start_date<=%s and service_end_date>=%s
select distinct parent from `tabPurchase Invoice Item`
where service_start_date<=%s and service_end_date>=%s
and enable_deferred_expense = 1 and docstatus = 1 and ifnull(amount, 0) > 0
''', (end_date or today(), start_date or add_months(today(), -1)))
''', (end_date, start_date))
# For each invoice, book deferred expense
for invoice in invoices:
doc = frappe.get_doc("Purchase Invoice", invoice)
book_deferred_income_or_expense(doc, start_date, end_date)
book_deferred_income_or_expense(doc, end_date)
def convert_deferred_revenue_to_income(start_date=None, end_date=None):
# book the expense/income on the last day, but it will be trigger on the 1st of month at 12:00 AM
if not start_date:
start_date = add_months(today(), -1)
if not end_date:
end_date = add_days(today(), -1)
# check for the sales invoice for which GL entries has to be done
invoices = frappe.db.sql_list('''
select distinct parent from `tabSales Invoice Item` where service_start_date<=%s and service_end_date>=%s
select distinct parent from `tabSales Invoice Item`
where service_start_date<=%s and service_end_date>=%s
and enable_deferred_revenue = 1 and docstatus = 1 and ifnull(amount, 0) > 0
''', (end_date or today(), start_date or add_months(today(), -1)))
''', (end_date, start_date))
# For each invoice, book deferred revenue
for invoice in invoices:
doc = frappe.get_doc("Sales Invoice", invoice)
book_deferred_income_or_expense(doc, start_date, end_date)
book_deferred_income_or_expense(doc, end_date)
def get_booking_dates(doc, item, posting_date=None):
if not posting_date:
posting_date = add_days(today(), -1)
last_gl_entry = False
def get_booking_dates(doc, item, start_date=None, end_date=None):
deferred_account = "deferred_revenue_account" if doc.doctype=="Sales Invoice" else "deferred_expense_account"
last_gl_entry, skip = False, False
booking_end_date = getdate(add_days(today(), -1) if not end_date else end_date)
if booking_end_date < item.service_start_date or \
(item.service_stop_date and booking_end_date.month > item.service_stop_date.month):
return None, None, None, True
elif booking_end_date >= item.service_end_date:
last_gl_entry = True
booking_end_date = item.service_end_date
elif item.service_stop_date and item.service_stop_date <= booking_end_date:
last_gl_entry = True
booking_end_date = item.service_stop_date
booking_start_date = getdate(add_months(today(), -1) if not start_date else start_date)
booking_start_date = booking_start_date \
if booking_start_date > item.service_start_date else item.service_start_date
prev_gl_entry = frappe.db.sql('''
select name, posting_date from `tabGL Entry` where company=%s and account=%s and
@ -81,17 +83,28 @@ def get_booking_dates(doc, item, start_date=None, end_date=None):
order by posting_date desc limit 1
''', (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True)
if not prev_gl_entry and item.service_start_date < booking_start_date:
booking_start_date = item.service_start_date
elif prev_gl_entry:
booking_start_date = getdate(add_days(prev_gl_entry[0].posting_date, 1))
skip = True if booking_start_date > booking_end_date else False
if prev_gl_entry:
start_date = getdate(add_days(prev_gl_entry[0].posting_date, 1))
else:
start_date = item.service_start_date
return last_gl_entry, booking_start_date, booking_end_date, skip
end_date = get_last_day(start_date)
if end_date >= item.service_end_date:
end_date = item.service_end_date
last_gl_entry = True
elif item.service_stop_date and end_date >= item.service_stop_date:
end_date = item.service_stop_date
last_gl_entry = True
def calculate_amount_and_base_amount(doc, item, last_gl_entry, total_days, total_booking_days):
account_currency = get_account_currency(item.expense_account)
if end_date > getdate(posting_date):
end_date = posting_date
if getdate(start_date) <= getdate(end_date):
return start_date, end_date, last_gl_entry
else:
return None, None, None
def calculate_amount(doc, item, last_gl_entry, total_days, total_booking_days, account_currency):
if doc.doctype == "Sales Invoice":
total_credit_debit, total_credit_debit_currency = "debit", "debit_in_account_currency"
deferred_account = "deferred_revenue_account"
@ -123,28 +136,15 @@ def calculate_amount_and_base_amount(doc, item, last_gl_entry, total_days, total
return amount, base_amount
def book_deferred_income_or_expense(doc, start_date=None, end_date=None):
# book the expense/income on the last day, but it will be trigger on the 1st of month at 12:00 AM
# start_date: 1st of the last month or the start date
# end_date: end_date or today-1
def book_deferred_income_or_expense(doc, posting_date=None):
enable_check = "enable_deferred_revenue" \
if doc.doctype=="Sales Invoice" else "enable_deferred_expense"
gl_entries = []
for item in doc.get('items'):
if not item.get(enable_check): continue
skip = False
last_gl_entry, booking_start_date, booking_end_date, skip = \
get_booking_dates(doc, item, start_date, end_date)
if skip: continue
total_days = date_diff(item.service_end_date, item.service_start_date) + 1
total_booking_days = date_diff(booking_end_date, booking_start_date) + 1
def _book_deferred_revenue_or_expense(item):
start_date, end_date, last_gl_entry = get_booking_dates(doc, item, posting_date=posting_date)
if not (start_date and end_date): return
account_currency = get_account_currency(item.expense_account)
amount, base_amount = calculate_amount_and_base_amount(doc, item, last_gl_entry, total_days, total_booking_days)
if doc.doctype == "Sales Invoice":
against, project = doc.customer, doc.project
credit_account, debit_account = item.income_account, item.deferred_revenue_account
@ -152,16 +152,38 @@ def book_deferred_income_or_expense(doc, start_date=None, end_date=None):
against, project = doc.supplier, item.project
credit_account, debit_account = item.deferred_expense_account, item.expense_account
total_days = date_diff(item.service_end_date, item.service_start_date) + 1
total_booking_days = date_diff(end_date, start_date) + 1
amount, base_amount = calculate_amount(doc, item, last_gl_entry,
total_days, total_booking_days, account_currency)
make_gl_entries(doc, credit_account, debit_account, against,
amount, base_amount, end_date, project, account_currency, item.cost_center, item.name)
if getdate(end_date) < getdate(posting_date) and not last_gl_entry:
_book_deferred_revenue_or_expense(item)
for item in doc.get('items'):
if item.get(enable_check):
_book_deferred_revenue_or_expense(item)
def make_gl_entries(doc, credit_account, debit_account, against,
amount, base_amount, posting_date, project, account_currency, cost_center, voucher_detail_no):
# GL Entry for crediting the amount in the deferred expense
from erpnext.accounts.general_ledger import make_gl_entries
gl_entries = []
gl_entries.append(
doc.get_gl_dict({
"account": credit_account,
"against": against,
"credit": base_amount,
"credit_in_account_currency": amount,
"cost_center": item.cost_center,
"voucher_detail_no": item.name,
'posting_date': booking_end_date,
"cost_center": cost_center,
"voucher_detail_no": voucher_detail_no,
'posting_date': posting_date,
'project': project
}, account_currency)
)
@ -172,16 +194,20 @@ def book_deferred_income_or_expense(doc, start_date=None, end_date=None):
"against": against,
"debit": base_amount,
"debit_in_account_currency": amount,
"cost_center": item.cost_center,
"voucher_detail_no": item.name,
'posting_date': booking_end_date,
"cost_center": cost_center,
"voucher_detail_no": voucher_detail_no,
'posting_date': posting_date,
'project': project
}, account_currency)
)
if gl_entries:
try:
make_gl_entries(gl_entries, cancel=(doc.docstatus == 2), merge_entries=True)
frappe.db.commit()
except:
frappe.db.rollback()
frappe.log_error(message = frappe.get_traceback(), title = _("Error while processing deferred accounting for {0}").format(doc.name))
title = _("Error while processing deferred accounting for {0}").format(doc.name)
traceback = frappe.get_traceback()
frappe.log_error(message=traceback , title=title)
sendmail_to_system_managers(title, traceback)

View File

@ -181,6 +181,10 @@ def get_inventory_account(company, warehouse=None):
return account
def create_account(**kwargs):
account = frappe.db.get_value("Account", filters={"account_name": kwargs.get("account_name"), "company": kwargs.get("company")})
if account:
return account
else:
account = frappe.get_doc(dict(
doctype = "Account",
account_name = kwargs.get('account_name'),

View File

@ -14,8 +14,9 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_per
from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency
from erpnext.stock.doctype.serial_no.serial_no import SerialNoWarehouseError
from frappe.model.naming import make_autoname
from erpnext.accounts.doctype.account.test_account import get_inventory_account
from erpnext.accounts.doctype.account.test_account import get_inventory_account, create_account
from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_data
from erpnext.stock.doctype.item.test_item import create_item
from six import iteritems
class TestSalesInvoice(unittest.TestCase):
def make(self):
@ -1514,6 +1515,56 @@ class TestSalesInvoice(unittest.TestCase):
accounts_settings.allow_cost_center_in_entry_of_bs_account = 0
accounts_settings.save()
def test_deferred_revenue(self):
deferred_account = create_account(account_name="Deferred Revenue",
parent_account="Current Liabilities - _TC", company="_Test Company")
item = create_item("_Test Item for Deferred Accounting")
item.enable_deferred_revenue = 1
item.deferred_revenue_account = deferred_account
item.no_of_months = 12
item.save()
si = create_sales_invoice(item=item.name, posting_date="2019-01-10", do_not_submit=True)
si.items[0].enable_deferred_revenue = 1
si.items[0].service_start_date = "2019-01-10"
si.items[0].service_end_date = "2019-03-15"
si.items[0].deferred_revenue_account = deferred_account
si.save()
si.submit()
from erpnext.accounts.deferred_revenue import convert_deferred_revenue_to_income
convert_deferred_revenue_to_income(start_date="2019-01-01", end_date="2019-01-31")
expected_gle = [
[deferred_account, 33.85, 0.0, "2019-01-31"],
["Sales - _TC", 0.0, 33.85, "2019-01-31"]
]
self.check_gl_entries(si.name, expected_gle, "2019-01-10")
convert_deferred_revenue_to_income(start_date="2019-01-01", end_date="2019-03-31")
expected_gle = [
[deferred_account, 43.08, 0.0, "2019-02-28"],
["Sales - _TC", 0.0, 43.08, "2019-02-28"],
[deferred_account, 23.07, 0.0, "2019-03-15"],
["Sales - _TC", 0.0, 23.07, "2019-03-15"]
]
self.check_gl_entries(si.name, expected_gle, "2019-01-31")
def check_gl_entries(self, voucher_no, expected_gle, posting_date):
gl_entries = frappe.db.sql("""select account, debit, credit, posting_date
from `tabGL Entry`
where voucher_type='Sales Invoice' and voucher_no=%s and posting_date > %s
order by posting_date asc, account asc""", (voucher_no, posting_date), as_dict=1)
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_gle[i][0], gle.account)
self.assertEqual(expected_gle[i][1], gle.debit)
self.assertEqual(expected_gle[i][2], gle.credit)
self.assertEqual(getdate(expected_gle[i][3]), gle.posting_date)
def create_sales_invoice(**args):
si = frappe.new_doc("Sales Invoice")

View File

@ -393,3 +393,6 @@ def create_item(item_code, is_stock_item=None, valuation_rate=0, warehouse=None,
"company": "_Test Company"
})
item.save()
else:
item = frappe.get_doc("Item", item_code)
return item