From 0a90ce5cf61df378884e835933f488be0e416411 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 28 Mar 2019 19:43:02 +0530 Subject: [PATCH 1/2] Refactored deferred revenue booking logic --- erpnext/accounts/deferred_revenue.py | 185 ++++++++++-------- .../accounts/doctype/account/test_account.py | 26 +-- .../sales_invoice/test_sales_invoice.py | 57 +++++- erpnext/stock/doctype/item/test_item.py | 3 + 4 files changed, 178 insertions(+), 93 deletions(-) diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index e638fc7b17..e146ae9c03 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -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,16 @@ 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 + last_gl_entry = False + end_date = None + 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,36 +153,62 @@ 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 - # GL Entry for crediting the amount in the deferred expense - 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, - 'project': project - }, account_currency) - ) - # GL Entry to debit the amount from the expense - gl_entries.append( - doc.get_gl_dict({ - "account": debit_account, - "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, - 'project': project - }, account_currency) - ) + 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": cost_center, + "voucher_detail_no": voucher_detail_no, + 'posting_date': posting_date, + 'project': project + }, account_currency) + ) + # GL Entry to debit the amount from the expense + gl_entries.append( + doc.get_gl_dict({ + "account": debit_account, + "against": against, + "debit": base_amount, + "debit_in_account_currency": amount, + "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)) \ No newline at end of file + 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) \ No newline at end of file diff --git a/erpnext/accounts/doctype/account/test_account.py b/erpnext/accounts/doctype/account/test_account.py index 4c057d9549..90177c630a 100644 --- a/erpnext/accounts/doctype/account/test_account.py +++ b/erpnext/accounts/doctype/account/test_account.py @@ -144,7 +144,7 @@ def _make_test_records(verbose): # related to Account Inventory Integration ["_Test Account Stock In Hand", "Current Assets", 0, None, None], - + # fixed asset depreciation ["_Test Fixed Asset", "Current Assets", 0, "Fixed Asset", None], ["_Test Accumulated Depreciations", "Current Assets", 0, None, None], @@ -181,13 +181,17 @@ def get_inventory_account(company, warehouse=None): return account def create_account(**kwargs): - account = frappe.get_doc(dict( - doctype = "Account", - account_name = kwargs.get('account_name'), - account_type = kwargs.get('account_type'), - parent_account = kwargs.get('parent_account'), - company = kwargs.get('company') - )) - - account.save() - return account.name + 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'), + account_type = kwargs.get('account_type'), + parent_account = kwargs.get('parent_account'), + company = kwargs.get('company') + )) + + account.save() + return account.name diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index f9364e2d84..45a29505fc 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -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): @@ -762,7 +763,7 @@ class TestSalesInvoice(unittest.TestCase): set_perpetual_inventory(0) frappe.db.sql("delete from `tabPOS Profile`") - + def test_pos_si_without_payment(self): set_perpetual_inventory() make_pos_profile() @@ -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") @@ -1611,4 +1662,4 @@ def get_outstanding_amount(against_voucher_type, against_voucher, account, party if against_voucher_type == 'Purchase Invoice': bal = bal * -1 - return bal + return bal \ No newline at end of file diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index d02559ebcb..aa67c33346 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -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 From 86fb523ea717ed4b913f15fa11bd8e7fe59ce10f Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 28 Mar 2019 20:06:57 +0530 Subject: [PATCH 2/2] fix(accounts): codacy fixes --- erpnext/accounts/deferred_revenue.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index e146ae9c03..b8d360a424 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -139,8 +139,7 @@ def calculate_amount(doc, item, last_gl_entry, total_days, total_booking_days, a def book_deferred_income_or_expense(doc, posting_date=None): enable_check = "enable_deferred_revenue" \ if doc.doctype=="Sales Invoice" else "enable_deferred_expense" - last_gl_entry = False - end_date = None + 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