From 8f04945cef6e39db2ee41a63e1006ca180aba963 Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Tue, 22 Aug 2023 12:36:09 +0530 Subject: [PATCH] fix: incorrect schedule in asset value adjustment (#36747) --- erpnext/assets/doctype/asset/asset.py | 2 +- erpnext/assets/doctype/asset/test_asset.py | 1 + .../asset_depreciation_schedule.py | 104 +++++++++++++++--- .../asset_value_adjustment.py | 99 +++++------------ .../test_asset_value_adjustment.py | 54 ++++++--- erpnext/controllers/buying_controller.py | 2 +- 6 files changed, 159 insertions(+), 103 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 2060c6ca835..ce894eb00d9 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -56,7 +56,6 @@ class Asset(AccountsController): def on_submit(self): self.validate_in_use_date() - self.set_status() self.make_asset_movement() if not self.booked_fixed_asset and self.validate_make_gl_entry(): self.make_gl_entries() @@ -72,6 +71,7 @@ class Asset(AccountsController): "Asset Depreciation Schedules created:
{0}

Please check, edit if needed, and submit the Asset." ).format(asset_depr_schedules_links) ) + self.set_status() add_asset_activity(self.name, _("Asset submitted")) def on_cancel(self): diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index cd66f1d136a..90eae2db388 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -19,6 +19,7 @@ from frappe.utils import ( from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.assets.doctype.asset.asset import ( + get_asset_value_after_depreciation, make_sales_invoice, split_asset, update_maintenance_status, diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py index 39ebd4ec0ec..2b4b248a302 100644 --- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py @@ -107,7 +107,7 @@ class AssetDepreciationSchedule(Document): have_asset_details_been_modified, not_manual_depr_or_have_manual_depr_details_been_modified ): self.make_depr_schedule(asset_doc, row, date_of_disposal, update_asset_finance_book_row) - self.set_accumulated_depreciation(row, date_of_disposal, date_of_return) + self.set_accumulated_depreciation(asset_doc, row, date_of_disposal, date_of_return) def have_asset_details_been_modified(self, asset_doc): return ( @@ -157,7 +157,12 @@ class AssetDepreciationSchedule(Document): self.status = "Draft" def make_depr_schedule( - self, asset_doc, row, date_of_disposal, update_asset_finance_book_row=True + self, + asset_doc, + row, + date_of_disposal, + update_asset_finance_book_row=True, + value_after_depreciation=None, ): if not self.get("depreciation_schedule"): self.depreciation_schedule = [] @@ -167,7 +172,9 @@ class AssetDepreciationSchedule(Document): start = self.clear_depr_schedule() - self._make_depr_schedule(asset_doc, row, start, date_of_disposal, update_asset_finance_book_row) + self._make_depr_schedule( + asset_doc, row, start, date_of_disposal, update_asset_finance_book_row, value_after_depreciation + ) def clear_depr_schedule(self): start = 0 @@ -187,23 +194,30 @@ class AssetDepreciationSchedule(Document): return start def _make_depr_schedule( - self, asset_doc, row, start, date_of_disposal, update_asset_finance_book_row + self, + asset_doc, + row, + start, + date_of_disposal, + update_asset_finance_book_row, + value_after_depreciation, ): asset_doc.validate_asset_finance_books(row) - value_after_depreciation = _get_value_after_depreciation_for_making_schedule(asset_doc, row) + if not value_after_depreciation: + value_after_depreciation = _get_value_after_depreciation_for_making_schedule(asset_doc, row) row.value_after_depreciation = value_after_depreciation if update_asset_finance_book_row: row.db_update() - number_of_pending_depreciations = cint(row.total_number_of_depreciations) - cint( + final_number_of_depreciations = cint(row.total_number_of_depreciations) - cint( self.number_of_depreciations_booked ) has_pro_rata = _check_is_pro_rata(asset_doc, row) if has_pro_rata: - number_of_pending_depreciations += 1 + final_number_of_depreciations += 1 has_wdv_or_dd_non_yearly_pro_rata = False if ( @@ -219,7 +233,9 @@ class AssetDepreciationSchedule(Document): depreciation_amount = 0 - for n in range(start, number_of_pending_depreciations): + number_of_pending_depreciations = final_number_of_depreciations - start + + for n in range(start, final_number_of_depreciations): # If depreciation is already completed (for double declining balance) if skip_row: continue @@ -236,10 +252,11 @@ class AssetDepreciationSchedule(Document): n, prev_depreciation_amount, has_wdv_or_dd_non_yearly_pro_rata, + number_of_pending_depreciations, ) if not has_pro_rata or ( - n < (cint(number_of_pending_depreciations) - 1) or number_of_pending_depreciations == 2 + n < (cint(final_number_of_depreciations) - 1) or final_number_of_depreciations == 2 ): schedule_date = add_months( row.depreciation_start_date, n * cint(row.frequency_of_depreciation) @@ -310,7 +327,7 @@ class AssetDepreciationSchedule(Document): ) # For last row - elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1: + elif has_pro_rata and n == cint(final_number_of_depreciations) - 1: if not asset_doc.flags.increase_in_asset_life: # In case of increase_in_asset_life, the asset.to_date is already set on asset_repair submission asset_doc.to_date = add_months( @@ -343,7 +360,7 @@ class AssetDepreciationSchedule(Document): # Adjust depreciation amount in the last period based on the expected value after useful life if row.expected_value_after_useful_life and ( ( - n == cint(number_of_pending_depreciations) - 1 + n == cint(final_number_of_depreciations) - 1 and value_after_depreciation != row.expected_value_after_useful_life ) or value_after_depreciation < row.expected_value_after_useful_life @@ -392,6 +409,7 @@ class AssetDepreciationSchedule(Document): def set_accumulated_depreciation( self, + asset_doc, row, date_of_disposal=None, date_of_return=None, @@ -403,13 +421,21 @@ class AssetDepreciationSchedule(Document): if self.depreciation_method == "Straight Line" or self.depreciation_method == "Manual" ] - accumulated_depreciation = flt(self.opening_accumulated_depreciation) + accumulated_depreciation = None value_after_depreciation = flt(row.value_after_depreciation) for i, d in enumerate(self.get("depreciation_schedule")): if ignore_booked_entry and d.journal_entry: continue + if not accumulated_depreciation: + if i > 0 and asset_doc.flags.decrease_in_asset_value_due_to_value_adjustment: + accumulated_depreciation = self.get("depreciation_schedule")[ + i - 1 + ].accumulated_depreciation_amount + else: + accumulated_depreciation = flt(self.opening_accumulated_depreciation) + depreciation_amount = flt(d.depreciation_amount, d.precision("depreciation_amount")) value_after_depreciation -= flt(depreciation_amount) @@ -507,9 +533,12 @@ def get_depreciation_amount( schedule_idx=0, prev_depreciation_amount=0, has_wdv_or_dd_non_yearly_pro_rata=False, + number_of_pending_depreciations=0, ): if fb_row.depreciation_method in ("Straight Line", "Manual"): - return get_straight_line_or_manual_depr_amount(asset, fb_row, schedule_idx) + return get_straight_line_or_manual_depr_amount( + asset, fb_row, schedule_idx, number_of_pending_depreciations + ) else: rate_of_depreciation = get_updated_rate_of_depreciation_for_wdv_and_dd( asset, depreciable_value, fb_row @@ -529,7 +558,9 @@ def get_updated_rate_of_depreciation_for_wdv_and_dd(asset, depreciable_value, fb return fb_row.rate_of_depreciation -def get_straight_line_or_manual_depr_amount(asset, row, schedule_idx): +def get_straight_line_or_manual_depr_amount( + asset, row, schedule_idx, number_of_pending_depreciations +): # if the Depreciation Schedule is being modified after Asset Repair due to increase in asset life and value if asset.flags.increase_in_asset_life: return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / ( @@ -540,6 +571,36 @@ def get_straight_line_or_manual_depr_amount(asset, row, schedule_idx): return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / flt( row.total_number_of_depreciations ) + # if the Depreciation Schedule is being modified after Asset Value Adjustment due to decrease in asset value + elif asset.flags.decrease_in_asset_value_due_to_value_adjustment: + if row.daily_depreciation: + daily_depr_amount = ( + flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life) + ) / date_diff( + add_months( + row.depreciation_start_date, + flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked) + * row.frequency_of_depreciation, + ), + add_months( + row.depreciation_start_date, + flt( + row.total_number_of_depreciations + - asset.number_of_depreciations_booked + - number_of_pending_depreciations + ) + * row.frequency_of_depreciation, + ), + ) + to_date = add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation) + from_date = add_months( + row.depreciation_start_date, (schedule_idx - 1) * row.frequency_of_depreciation + ) + return daily_depr_amount * date_diff(to_date, from_date) + else: + return ( + flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life) + ) / number_of_pending_depreciations # if the Depreciation Schedule is being prepared for the first time else: if row.daily_depreciation: @@ -669,7 +730,12 @@ def cancel_asset_depr_schedules(asset_doc): def make_new_active_asset_depr_schedules_and_cancel_current_ones( - asset_doc, notes, date_of_disposal=None, date_of_return=None + asset_doc, + notes, + date_of_disposal=None, + date_of_return=None, + value_after_depreciation=None, + ignore_booked_entry=False, ): for row in asset_doc.get("finance_books"): current_asset_depr_schedule_doc = get_asset_depr_schedule_doc( @@ -695,8 +761,12 @@ def make_new_active_asset_depr_schedules_and_cancel_current_ones( row.rate_of_depreciation = new_rate_of_depreciation new_asset_depr_schedule_doc.rate_of_depreciation = new_rate_of_depreciation - new_asset_depr_schedule_doc.make_depr_schedule(asset_doc, row, date_of_disposal) - new_asset_depr_schedule_doc.set_accumulated_depreciation(row, date_of_disposal, date_of_return) + new_asset_depr_schedule_doc.make_depr_schedule( + asset_doc, row, date_of_disposal, value_after_depreciation=value_after_depreciation + ) + new_asset_depr_schedule_doc.set_accumulated_depreciation( + asset_doc, row, date_of_disposal, date_of_return, ignore_booked_entry + ) new_asset_depr_schedule_doc.notes = notes diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py index 823b6e9e21d..9be7243602e 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py @@ -5,7 +5,7 @@ import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import date_diff, flt, formatdate, get_link_to_form, getdate +from frappe.utils import flt, formatdate, get_link_to_form, getdate from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_checks_for_pl_and_bs_accounts, @@ -14,8 +14,7 @@ from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciatio from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( - get_asset_depr_schedule_doc, - get_depreciation_amount, + make_new_active_asset_depr_schedules_and_cancel_current_ones, ) @@ -27,7 +26,7 @@ class AssetValueAdjustment(Document): def on_submit(self): self.make_depreciation_entry() - self.reschedule_depreciations(self.new_asset_value) + self.update_asset(self.new_asset_value) add_asset_activity( self.asset, _("Asset's value adjusted after submission of Asset Value Adjustment {0}").format( @@ -36,7 +35,7 @@ class AssetValueAdjustment(Document): ) def on_cancel(self): - self.reschedule_depreciations(self.current_asset_value) + self.update_asset(self.current_asset_value) add_asset_activity( self.asset, _("Asset's value adjusted after cancellation of Asset Value Adjustment {0}").format( @@ -124,73 +123,33 @@ class AssetValueAdjustment(Document): self.db_set("journal_entry", je.name) - def reschedule_depreciations(self, asset_value): + def update_asset(self, asset_value): asset = frappe.get_doc("Asset", self.asset) - country = frappe.get_value("Company", self.company, "country") - for d in asset.finance_books: - d.value_after_depreciation = asset_value + if not asset.calculate_depreciation: + asset.value_after_depreciation = asset_value + asset.save() + return - current_asset_depr_schedule_doc = get_asset_depr_schedule_doc( - asset.name, "Active", d.finance_book + asset.flags.decrease_in_asset_value_due_to_value_adjustment = True + + if self.docstatus == 1: + notes = _( + "This schedule was created when Asset {0} was adjusted through Asset Value Adjustment {1}." + ).format( + get_link_to_form("Asset", asset.name), + get_link_to_form(self.get("doctype"), self.get("name")), + ) + elif self.docstatus == 2: + notes = _( + "This schedule was created when Asset {0}'s Asset Value Adjustment {1} was cancelled." + ).format( + get_link_to_form("Asset", asset.name), + get_link_to_form(self.get("doctype"), self.get("name")), ) - new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc) - new_asset_depr_schedule_doc.status = "Draft" - new_asset_depr_schedule_doc.docstatus = 0 - - current_asset_depr_schedule_doc.flags.should_not_cancel_depreciation_entries = True - current_asset_depr_schedule_doc.cancel() - - if self.docstatus == 1: - notes = _( - "This schedule was created when Asset {0} was adjusted through Asset Value Adjustment {1}." - ).format( - get_link_to_form(asset.doctype, asset.name), - get_link_to_form(self.get("doctype"), self.get("name")), - ) - elif self.docstatus == 2: - notes = _( - "This schedule was created when Asset {0}'s Asset Value Adjustment {1} was cancelled." - ).format( - get_link_to_form(asset.doctype, asset.name), - get_link_to_form(self.get("doctype"), self.get("name")), - ) - new_asset_depr_schedule_doc.notes = notes - - new_asset_depr_schedule_doc.insert() - - depr_schedule = new_asset_depr_schedule_doc.get("depreciation_schedule") - - if d.depreciation_method in ("Straight Line", "Manual"): - end_date = max(s.schedule_date for s in depr_schedule) - total_days = date_diff(end_date, self.date) - rate_per_day = flt(d.value_after_depreciation - d.expected_value_after_useful_life) / flt( - total_days - ) - from_date = self.date - else: - no_of_depreciations = len([s.name for s in depr_schedule if not s.journal_entry]) - - value_after_depreciation = d.value_after_depreciation - for data in depr_schedule: - if not data.journal_entry: - if d.depreciation_method in ("Straight Line", "Manual"): - days = date_diff(data.schedule_date, from_date) - depreciation_amount = days * rate_per_day - from_date = data.schedule_date - else: - depreciation_amount = get_depreciation_amount(asset, value_after_depreciation, d) - - if depreciation_amount: - value_after_depreciation -= flt(depreciation_amount) - data.depreciation_amount = depreciation_amount - - d.db_update() - - new_asset_depr_schedule_doc.set_accumulated_depreciation(d, ignore_booked_entry=True) - for asset_data in depr_schedule: - if not asset_data.journal_entry: - asset_data.db_update() - - new_asset_depr_schedule_doc.submit() + make_new_active_asset_depr_schedules_and_cancel_current_ones( + asset, notes, value_after_depreciation=asset_value, ignore_booked_entry=True + ) + asset.flags.ignore_validate_update_after_submit = True + asset.save() diff --git a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py index 0b3dcba024c..5d49759727c 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py @@ -4,9 +4,10 @@ import unittest import frappe -from frappe.utils import add_days, get_last_day, nowdate +from frappe.utils import add_days, cstr, get_last_day, getdate, nowdate from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation +from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries from erpnext.assets.doctype.asset.test_asset import create_asset_data from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( get_asset_depr_schedule_doc, @@ -49,27 +50,23 @@ class TestAssetValueAdjustment(unittest.TestCase): def test_asset_depreciation_value_adjustment(self): pr = make_purchase_receipt( - item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location" + item_code="Macbook Pro", qty=1, rate=120000.0, location="Test Location" ) asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name") asset_doc = frappe.get_doc("Asset", asset_name) asset_doc.calculate_depreciation = 1 + asset_doc.available_for_use_date = "2023-01-15" + asset_doc.purchase_date = "2023-01-15" - month_end_date = get_last_day(nowdate()) - purchase_date = nowdate() if nowdate() != month_end_date else add_days(nowdate(), -15) - - asset_doc.available_for_use_date = purchase_date - asset_doc.purchase_date = purchase_date - asset_doc.calculate_depreciation = 1 asset_doc.append( "finance_books", { "expected_value_after_useful_life": 200, "depreciation_method": "Straight Line", - "total_number_of_depreciations": 3, - "frequency_of_depreciation": 10, - "depreciation_start_date": month_end_date, + "total_number_of_depreciations": 12, + "frequency_of_depreciation": 1, + "depreciation_start_date": "2023-01-31", }, ) asset_doc.submit() @@ -77,9 +74,15 @@ class TestAssetValueAdjustment(unittest.TestCase): first_asset_depr_schedule = get_asset_depr_schedule_doc(asset_doc.name, "Active") self.assertEquals(first_asset_depr_schedule.status, "Active") + post_depreciation_entries(getdate("2023-08-21")) + current_value = get_asset_value_after_depreciation(asset_doc.name) + adj_doc = make_asset_value_adjustment( - asset=asset_doc.name, current_asset_value=current_value, new_asset_value=50000.0 + asset=asset_doc.name, + current_asset_value=current_value, + new_asset_value=50000.0, + date="2023-08-21", ) adj_doc.submit() @@ -90,8 +93,8 @@ class TestAssetValueAdjustment(unittest.TestCase): self.assertEquals(first_asset_depr_schedule.status, "Cancelled") expected_gle = ( - ("_Test Accumulated Depreciations - _TC", 0.0, 50000.0), - ("_Test Depreciations - _TC", 50000.0, 0.0), + ("_Test Accumulated Depreciations - _TC", 0.0, 4625.29), + ("_Test Depreciations - _TC", 4625.29, 0.0), ) gle = frappe.db.sql( @@ -103,6 +106,29 @@ class TestAssetValueAdjustment(unittest.TestCase): self.assertSequenceEqual(gle, expected_gle) + expected_schedules = [ + ["2023-01-31", 5474.73, 5474.73], + ["2023-02-28", 9983.33, 15458.06], + ["2023-03-31", 9983.33, 25441.39], + ["2023-04-30", 9983.33, 35424.72], + ["2023-05-31", 9983.33, 45408.05], + ["2023-06-30", 9983.33, 55391.38], + ["2023-07-31", 9983.33, 65374.71], + ["2023-08-31", 8300.0, 73674.71], + ["2023-09-30", 8300.0, 81974.71], + ["2023-10-31", 8300.0, 90274.71], + ["2023-11-30", 8300.0, 98574.71], + ["2023-12-31", 8300.0, 106874.71], + ["2024-01-15", 8300.0, 115174.71], + ] + + schedules = [ + [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] + for d in second_asset_depr_schedule.get("depreciation_schedule") + ] + + self.assertEqual(schedules, expected_schedules) + def make_asset_value_adjustment(**args): args = frappe._dict(args) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 7b7c53ecfe1..b396b27da7a 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -759,7 +759,7 @@ class BuyingController(SubcontractingController): "company": self.company, "supplier": self.supplier, "purchase_date": self.posting_date, - "calculate_depreciation": 1, + "calculate_depreciation": 0, "purchase_receipt_amount": purchase_amount, "gross_purchase_amount": purchase_amount, "asset_quantity": row.qty if is_grouped_asset else 0,