diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 72e9790700f..dde3947bd9d 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -7,17 +7,7 @@ from frappe import _, msgprint, throw from frappe.contacts.doctype.address.address import get_address_display from frappe.model.mapper import get_mapped_doc from frappe.model.utils import get_fetch_values -from frappe.utils import ( - add_days, - add_months, - cint, - cstr, - flt, - formatdate, - get_link_to_form, - getdate, - nowdate, -) +from frappe.utils import add_days, cint, cstr, flt, formatdate, get_link_to_form, getdate, nowdate from six import iteritems import erpnext @@ -33,10 +23,12 @@ from erpnext.accounts.general_ledger import get_round_off_account_and_cost_cente from erpnext.accounts.party import get_due_date, get_party_account, get_party_details from erpnext.accounts.utils import get_account_currency from erpnext.assets.doctype.asset.depreciation import ( + depreciate_asset, get_disposal_account_and_cost_center, get_gl_entries_on_asset_disposal, get_gl_entries_on_asset_regain, - make_depreciation_entry, + reset_depreciation_schedule, + reverse_depreciation_entry_made_after_disposal, ) from erpnext.controllers.accounts_controller import validate_account_head from erpnext.controllers.selling_controller import SellingController @@ -1114,18 +1106,20 @@ class SalesInvoice(SellingController): asset = self.get_asset(item) if self.is_return: - if asset.calculate_depreciation: - self.reverse_depreciation_entry_made_after_sale(asset) - self.reset_depreciation_schedule(asset) - fixed_asset_gl_entries = get_gl_entries_on_asset_regain( asset, item.base_net_amount, item.finance_book ) asset.db_set("disposal_date", None) + if asset.calculate_depreciation: + posting_date = frappe.db.get_value("Sales Invoice", self.return_against, "posting_date") + reverse_depreciation_entry_made_after_disposal(asset, posting_date) + reset_depreciation_schedule(asset, self.posting_date) + else: if asset.calculate_depreciation: - self.depreciate_asset(asset) + depreciate_asset(asset, self.posting_date) + asset.reload() fixed_asset_gl_entries = get_gl_entries_on_asset_disposal( asset, item.base_net_amount, item.finance_book @@ -1193,95 +1187,6 @@ class SalesInvoice(SellingController): _("Select finance book for the item {0} at row {1}").format(item.item_code, item.idx) ) - def depreciate_asset(self, asset): - asset.flags.ignore_validate_update_after_submit = True - asset.prepare_depreciation_data(date_of_sale=self.posting_date) - asset.save() - - make_depreciation_entry(asset.name, self.posting_date) - asset.load_from_db() - - def reset_depreciation_schedule(self, asset): - asset.flags.ignore_validate_update_after_submit = True - - # recreate original depreciation schedule of the asset - asset.prepare_depreciation_data(date_of_return=self.posting_date) - - self.modify_depreciation_schedule_for_asset_repairs(asset) - asset.save() - asset.load_from_db() - - def modify_depreciation_schedule_for_asset_repairs(self, asset): - asset_repairs = frappe.get_all( - "Asset Repair", filters={"asset": asset.name}, fields=["name", "increase_in_asset_life"] - ) - - for repair in asset_repairs: - if repair.increase_in_asset_life: - asset_repair = frappe.get_doc("Asset Repair", repair.name) - asset_repair.modify_depreciation_schedule() - asset.prepare_depreciation_data() - - def reverse_depreciation_entry_made_after_sale(self, asset): - from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry - - posting_date_of_original_invoice = self.get_posting_date_of_sales_invoice() - - row = -1 - finance_book = asset.get("schedules")[0].get("finance_book") - for schedule in asset.get("schedules"): - if schedule.finance_book != finance_book: - row = 0 - finance_book = schedule.finance_book - else: - row += 1 - - if schedule.schedule_date == posting_date_of_original_invoice: - if not self.sale_was_made_on_original_schedule_date( - asset, schedule, row, posting_date_of_original_invoice - ) or self.sale_happens_in_the_future(posting_date_of_original_invoice): - - reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry) - reverse_journal_entry.posting_date = nowdate() - frappe.flags.is_reverse_depr_entry = True - reverse_journal_entry.submit() - - frappe.flags.is_reverse_depr_entry = False - asset.flags.ignore_validate_update_after_submit = True - schedule.journal_entry = None - depreciation_amount = self.get_depreciation_amount_in_je(reverse_journal_entry) - asset.finance_books[0].value_after_depreciation += depreciation_amount - asset.save() - - def get_posting_date_of_sales_invoice(self): - return frappe.db.get_value("Sales Invoice", self.return_against, "posting_date") - - # if the invoice had been posted on the date the depreciation was initially supposed to happen, the depreciation shouldn't be undone - def sale_was_made_on_original_schedule_date( - self, asset, schedule, row, posting_date_of_original_invoice - ): - for finance_book in asset.get("finance_books"): - if schedule.finance_book == finance_book.finance_book: - orginal_schedule_date = add_months( - finance_book.depreciation_start_date, row * cint(finance_book.frequency_of_depreciation) - ) - - if orginal_schedule_date == posting_date_of_original_invoice: - return True - return False - - def sale_happens_in_the_future(self, posting_date_of_original_invoice): - if posting_date_of_original_invoice > getdate(): - return True - - return False - - def get_depreciation_amount_in_je(self, journal_entry): - if journal_entry.accounts[0].debit_in_account_currency: - return journal_entry.accounts[0].debit_in_account_currency - else: - return journal_entry.accounts[0].credit_in_account_currency - @property def enable_discount_accounting(self): if not hasattr(self, "_enable_discount_accounting"): diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 1473b79bea5..587cb147b79 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -202,6 +202,10 @@ frappe.ui.form.on('Asset', { }, setup_chart: function(frm) { + if(frm.doc.finance_books.length > 1) { + return + } + var x_intervals = [frm.doc.purchase_date]; var asset_values = [frm.doc.gross_purchase_amount]; var last_depreciation_date = frm.doc.purchase_date; diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 59187d36865..af32f93cf05 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -79,12 +79,12 @@ class Asset(AccountsController): _("Purchase Invoice cannot be made against an existing asset {0}").format(self.name) ) - def prepare_depreciation_data(self, date_of_sale=None, date_of_return=None): + def prepare_depreciation_data(self, date_of_disposal=None, date_of_return=None): if self.calculate_depreciation: self.value_after_depreciation = 0 self.set_depreciation_rate() - self.make_depreciation_schedule(date_of_sale) - self.set_accumulated_depreciation(date_of_sale, date_of_return) + self.make_depreciation_schedule(date_of_disposal) + self.set_accumulated_depreciation(date_of_disposal, date_of_return) else: self.finance_books = [] self.value_after_depreciation = flt(self.gross_purchase_amount) - flt( @@ -223,7 +223,7 @@ class Asset(AccountsController): self.get_depreciation_rate(d, on_validate=True), d.precision("rate_of_depreciation") ) - def make_depreciation_schedule(self, date_of_sale): + def make_depreciation_schedule(self, date_of_disposal): if "Manual" not in [d.depreciation_method for d in self.finance_books] and not self.get( "schedules" ): @@ -279,17 +279,17 @@ class Asset(AccountsController): monthly_schedule_date = add_months(schedule_date, -finance_book.frequency_of_depreciation + 1) # if asset is being sold - if date_of_sale: + if date_of_disposal: from_date = self.get_from_date(finance_book.finance_book) depreciation_amount, days, months = self.get_pro_rata_amt( - finance_book, depreciation_amount, from_date, date_of_sale + finance_book, depreciation_amount, from_date, date_of_disposal ) if depreciation_amount > 0: self.append( "schedules", { - "schedule_date": date_of_sale, + "schedule_date": date_of_disposal, "depreciation_amount": depreciation_amount, "depreciation_method": finance_book.depreciation_method, "finance_book": finance_book.finance_book, @@ -364,6 +364,9 @@ class Asset(AccountsController): }, ) + if len(self.get("finance_books")) > 1 and any(start): + self.sort_depreciation_schedule() + # depreciation schedules need to be cleared before modification due to increase in asset life/asset sales # JE: Journal Entry, FB: Finance Book def clear_depreciation_schedule(self): @@ -399,6 +402,14 @@ class Asset(AccountsController): return start + def sort_depreciation_schedule(self): + self.schedules = sorted( + self.schedules, key=lambda s: (int(s.finance_book_id), getdate(s.schedule_date)) + ) + + for idx, s in enumerate(self.schedules, 1): + s.idx = idx + def get_from_date(self, finance_book): if not self.get("schedules"): return self.available_for_use_date @@ -531,7 +542,7 @@ class Asset(AccountsController): return True def set_accumulated_depreciation( - self, date_of_sale=None, date_of_return=None, ignore_booked_entry=False + self, date_of_disposal=None, date_of_return=None, ignore_booked_entry=False ): straight_line_idx = [ d.idx for d in self.get("schedules") if d.depreciation_method == "Straight Line" @@ -554,7 +565,7 @@ class Asset(AccountsController): if ( straight_line_idx and i == max(straight_line_idx) - 1 - and not date_of_sale + and not date_of_disposal and not date_of_return ): book = self.get("finance_books")[cint(d.finance_book_id) - 1] diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index f8f581d8ae2..66dd2e98dc5 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -4,7 +4,7 @@ import frappe from frappe import _ -from frappe.utils import cint, flt, get_link_to_form, getdate, today +from frappe.utils import add_months, cint, flt, get_link_to_form, getdate, nowdate, today from frappe.utils.user import get_users_with_role from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( @@ -247,6 +247,11 @@ def scrap_asset(asset_name): _("Asset {0} cannot be scrapped, as it is already {1}").format(asset.name, asset.status) ) + date = today() + + depreciate_asset(asset, date) + asset.reload() + depreciation_series = frappe.get_cached_value( "Company", asset.company, "series_for_depreciation_entry" ) @@ -254,7 +259,7 @@ def scrap_asset(asset_name): je = frappe.new_doc("Journal Entry") je.voucher_type = "Journal Entry" je.naming_series = depreciation_series - je.posting_date = today() + je.posting_date = date je.company = asset.company je.remark = "Scrap Entry for asset {0}".format(asset_name) @@ -265,7 +270,7 @@ def scrap_asset(asset_name): je.flags.ignore_permissions = True je.submit() - frappe.db.set_value("Asset", asset_name, "disposal_date", today()) + frappe.db.set_value("Asset", asset_name, "disposal_date", date) frappe.db.set_value("Asset", asset_name, "journal_entry_for_scrap", je.name) asset.set_status("Scrapped") @@ -276,6 +281,9 @@ def scrap_asset(asset_name): def restore_asset(asset_name): asset = frappe.get_doc("Asset", asset_name) + reverse_depreciation_entry_made_after_disposal(asset, asset.disposal_date) + reset_depreciation_schedule(asset, asset.disposal_date) + je = asset.journal_entry_for_scrap asset.db_set("disposal_date", None) @@ -286,6 +294,96 @@ def restore_asset(asset_name): asset.set_status() +def depreciate_asset(asset, date): + asset.flags.ignore_validate_update_after_submit = True + asset.prepare_depreciation_data(date_of_disposal=date) + asset.save() + + make_depreciation_entry(asset.name, date) + + +def reset_depreciation_schedule(asset, date): + asset.flags.ignore_validate_update_after_submit = True + + # recreate original depreciation schedule of the asset + asset.prepare_depreciation_data(date_of_return=date) + + modify_depreciation_schedule_for_asset_repairs(asset) + asset.save() + + +def modify_depreciation_schedule_for_asset_repairs(asset): + asset_repairs = frappe.get_all( + "Asset Repair", filters={"asset": asset.name}, fields=["name", "increase_in_asset_life"] + ) + + for repair in asset_repairs: + if repair.increase_in_asset_life: + asset_repair = frappe.get_doc("Asset Repair", repair.name) + asset_repair.modify_depreciation_schedule() + asset.prepare_depreciation_data() + + +def reverse_depreciation_entry_made_after_disposal(asset, date): + from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry + + row = -1 + finance_book = asset.get("schedules")[0].get("finance_book") + for schedule in asset.get("schedules"): + if schedule.finance_book != finance_book: + row = 0 + finance_book = schedule.finance_book + else: + row += 1 + + if schedule.schedule_date == date: + if not disposal_was_made_on_original_schedule_date( + asset, schedule, row, date + ) or disposal_happens_in_the_future(date): + + reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry) + reverse_journal_entry.posting_date = nowdate() + frappe.flags.is_reverse_depr_entry = True + reverse_journal_entry.submit() + + frappe.flags.is_reverse_depr_entry = False + asset.flags.ignore_validate_update_after_submit = True + schedule.journal_entry = None + depreciation_amount = get_depreciation_amount_in_je(reverse_journal_entry) + + idx = cint(schedule.finance_book_id) + asset.finance_books[idx - 1].value_after_depreciation += depreciation_amount + + asset.save() + + +def get_depreciation_amount_in_je(journal_entry): + if journal_entry.accounts[0].debit_in_account_currency: + return journal_entry.accounts[0].debit_in_account_currency + else: + return journal_entry.accounts[0].credit_in_account_currency + + +# if the invoice had been posted on the date the depreciation was initially supposed to happen, the depreciation shouldn't be undone +def disposal_was_made_on_original_schedule_date(asset, schedule, row, posting_date_of_disposal): + for finance_book in asset.get("finance_books"): + if schedule.finance_book == finance_book.finance_book: + orginal_schedule_date = add_months( + finance_book.depreciation_start_date, row * cint(finance_book.frequency_of_depreciation) + ) + + if orginal_schedule_date == posting_date_of_disposal: + return True + return False + + +def disposal_happens_in_the_future(posting_date_of_disposal): + if posting_date_of_disposal > getdate(): + return True + + return False + + def get_gl_entries_on_asset_regain(asset, selling_amount=0, finance_book=None): ( fixed_asset_account, diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 26db6396df3..d133cebedb8 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -4,7 +4,16 @@ import unittest import frappe -from frappe.utils import add_days, add_months, cstr, flt, get_last_day, getdate, nowdate +from frappe.utils import ( + add_days, + add_months, + cstr, + flt, + get_first_day, + get_last_day, + getdate, + nowdate, +) from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.assets.doctype.asset.asset import make_sales_invoice, update_maintenance_status @@ -153,28 +162,59 @@ class TestAsset(AssetSetup): self.assertEqual(doc.items[0].is_fixed_asset, 1) def test_scrap_asset(self): + date = nowdate() + purchase_date = add_months(get_first_day(date), -2) + asset = create_asset( calculate_depreciation=1, - available_for_use_date="2020-01-01", - purchase_date="2020-01-01", + available_for_use_date=purchase_date, + purchase_date=purchase_date, expected_value_after_useful_life=10000, total_number_of_depreciations=10, frequency_of_depreciation=1, submit=1, ) - post_depreciation_entries(date=add_months("2020-01-01", 4)) + post_depreciation_entries(date=add_months(purchase_date, 2)) + asset.load_from_db() + + accumulated_depr_amount = flt( + asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation, + asset.precision("gross_purchase_amount"), + ) + self.assertEquals(accumulated_depr_amount, 18000.0) scrap_asset(asset.name) - asset.load_from_db() + + accumulated_depr_amount = flt( + asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation, + asset.precision("gross_purchase_amount"), + ) + pro_rata_amount, _, _ = asset.get_pro_rata_amt( + asset.finance_books[0], 9000, get_last_day(add_months(purchase_date, 1)), date + ) + pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount")) + self.assertEquals( + accumulated_depr_amount, + flt(18000.0 + pro_rata_amount, asset.precision("gross_purchase_amount")), + ) + self.assertEqual(asset.status, "Scrapped") self.assertTrue(asset.journal_entry_for_scrap) expected_gle = ( - ("_Test Accumulated Depreciations - _TC", 36000.0, 0.0), + ( + "_Test Accumulated Depreciations - _TC", + flt(18000.0 + pro_rata_amount, asset.precision("gross_purchase_amount")), + 0.0, + ), ("_Test Fixed Asset - _TC", 0.0, 100000.0), - ("_Test Gain/Loss on Asset Disposal - _TC", 64000.0, 0.0), + ( + "_Test Gain/Loss on Asset Disposal - _TC", + flt(82000.0 - pro_rata_amount, asset.precision("gross_purchase_amount")), + 0.0, + ), ) gle = frappe.db.sql( @@ -183,7 +223,7 @@ class TestAsset(AssetSetup): order by account""", asset.journal_entry_for_scrap, ) - self.assertEqual(gle, expected_gle) + self.assertSequenceEqual(gle, expected_gle) restore_asset(asset.name) @@ -191,34 +231,57 @@ class TestAsset(AssetSetup): self.assertFalse(asset.journal_entry_for_scrap) self.assertEqual(asset.status, "Partially Depreciated") + accumulated_depr_amount = flt( + asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation, + asset.precision("gross_purchase_amount"), + ) + this_month_depr_amount = 9000.0 if is_last_day_of_the_month(date) else 0 + + self.assertEquals(accumulated_depr_amount, 18000.0 + this_month_depr_amount) + def test_gle_made_by_asset_sale(self): + date = nowdate() + purchase_date = add_months(get_first_day(date), -2) + asset = create_asset( calculate_depreciation=1, - available_for_use_date="2020-06-06", - purchase_date="2020-01-01", + available_for_use_date=purchase_date, + purchase_date=purchase_date, expected_value_after_useful_life=10000, - total_number_of_depreciations=3, - frequency_of_depreciation=10, - depreciation_start_date="2020-12-31", + total_number_of_depreciations=10, + frequency_of_depreciation=1, submit=1, ) - post_depreciation_entries(date="2021-01-01") + + post_depreciation_entries(date=add_months(purchase_date, 2)) si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company") si.customer = "_Test Customer" - si.set_posting_time = 1 - si.posting_date = "2021-10-31" - si.due_date = "2021-10-31" - si.get("items")[0].rate = 75000 + si.due_date = nowdate() + si.get("items")[0].rate = 25000 + si.insert() si.submit() self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold") + pro_rata_amount, _, _ = asset.get_pro_rata_amt( + asset.finance_books[0], 9000, get_last_day(add_months(purchase_date, 1)), date + ) + pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount")) + expected_gle = ( - ("_Test Accumulated Depreciations - _TC", 50490.2, 0.0), + ( + "_Test Accumulated Depreciations - _TC", + flt(18000.0 + pro_rata_amount, asset.precision("gross_purchase_amount")), + 0.0, + ), ("_Test Fixed Asset - _TC", 0.0, 100000.0), - ("_Test Gain/Loss on Asset Disposal - _TC", 0.0, 25490.2), - ("Debtors - _TC", 75000.0, 0.0), + ( + "_Test Gain/Loss on Asset Disposal - _TC", + flt(57000.0 - pro_rata_amount, asset.precision("gross_purchase_amount")), + 0.0, + ), + ("Debtors - _TC", 25000.0, 0.0), ) gle = frappe.db.sql( @@ -228,14 +291,9 @@ class TestAsset(AssetSetup): si.name, ) - for i, gle_entry in enumerate(gle): - self.assertEqual(gle_entry[0], expected_gle[i][0]) - self.assertEqual(flt(gle_entry[1], 1), flt(expected_gle[i][1], 1)) - self.assertEqual(flt(gle_entry[2], 1), flt(expected_gle[i][2], 1)) + self.assertSequenceEqual(gle, expected_gle) - si.load_from_db() si.cancel() - self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated") def test_asset_with_maintenance_required_status_after_sale(self): @@ -1471,3 +1529,9 @@ def set_depreciation_settings_in_company(): def enable_cwip_accounting(asset_category, enable=1): frappe.db.set_value("Asset Category", asset_category, "enable_cwip_accounting", enable) + + +def is_last_day_of_the_month(dt): + last_day_of_the_month = get_last_day(dt) + + return getdate(dt) == getdate(last_day_of_the_month)