diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 68ac9829fb1..ea8b7d831b2 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -6,7 +6,7 @@ import json import frappe from frappe import _, msgprint, scrub -from frappe.utils import cint, cstr, flt, fmt_money, formatdate, get_link_to_form, nowdate +from frappe.utils import cstr, flt, fmt_money, formatdate, get_link_to_form, nowdate import erpnext from erpnext.accounts.deferred_revenue import get_deferred_booking_accounts @@ -23,6 +23,9 @@ from erpnext.accounts.utils import ( get_stock_accounts, get_stock_and_account_balance, ) +from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( + get_depr_schedule, +) from erpnext.controllers.accounts_controller import AccountsController @@ -283,16 +286,17 @@ class JournalEntry(AccountsController): for d in self.get("accounts"): if d.reference_type == "Asset" and d.reference_name: asset = frappe.get_doc("Asset", d.reference_name) - for s in asset.get("schedules"): - if s.journal_entry == self.name: - s.db_set("journal_entry", None) + for row in asset.get("finance_books"): + depr_schedule = get_depr_schedule(asset.name, "Active", row.finance_book) - idx = cint(s.finance_book_id) or 1 - finance_books = asset.get("finance_books")[idx - 1] - finance_books.value_after_depreciation += s.depreciation_amount - finance_books.db_update() + for s in depr_schedule or []: + if s.journal_entry == self.name: + s.db_set("journal_entry", None) - asset.set_status() + row.value_after_depreciation += s.depreciation_amount + row.db_update() + + asset.set_status() def unlink_inter_company_jv(self): if ( diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index c276be2b006..31cf1206ce3 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1185,11 +1185,24 @@ class SalesInvoice(SellingController): 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) + notes = _( + "This schedule was created when Asset {0} was returned after being sold through Sales Invoice {1}." + ).format( + get_link_to_form(asset.doctype, asset.name), + get_link_to_form(self.doctype, self.get("name")), + ) + reset_depreciation_schedule(asset, self.posting_date, notes) + asset.reload() else: if asset.calculate_depreciation: - depreciate_asset(asset, self.posting_date) + notes = _( + "This schedule was created when Asset {0} was sold through Sales Invoice {1}." + ).format( + get_link_to_form(asset.doctype, asset.name), + get_link_to_form(self.doctype, self.get("name")), + ) + depreciate_asset(asset, self.posting_date, notes) asset.reload() fixed_asset_gl_entries = get_gl_entries_on_asset_disposal( diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 855380ef25b..e96847e1b6e 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -21,6 +21,9 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_comp from erpnext.accounts.utils import PaymentEntryUnlinkError from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data +from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( + get_depr_schedule, +) from erpnext.controllers.accounts_controller import update_invoice_status from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_data from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency @@ -2774,7 +2777,7 @@ class TestSalesInvoice(unittest.TestCase): ["2021-09-30", 5041.1, 26407.22], ] - for i, schedule in enumerate(asset.schedules): + for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")): self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date) self.assertEqual(expected_values[i][1], schedule.depreciation_amount) self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount) @@ -2805,7 +2808,7 @@ class TestSalesInvoice(unittest.TestCase): expected_values = [["2020-12-31", 30000, 30000], ["2021-12-31", 30000, 60000]] - for i, schedule in enumerate(asset.schedules): + for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")): self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date) self.assertEqual(expected_values[i][1], schedule.depreciation_amount) self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount) @@ -2834,7 +2837,7 @@ class TestSalesInvoice(unittest.TestCase): ["2025-06-06", 18633.88, 100000.0, False], ] - for i, schedule in enumerate(asset.schedules): + for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")): self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date) self.assertEqual(expected_values[i][1], schedule.depreciation_amount) self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount) diff --git a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py index ad9b1ba58eb..43b95dca80e 100644 --- a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py +++ b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py @@ -131,8 +131,8 @@ def get_assets(filters): else 0 end), 0) as depreciation_amount_during_the_period - from `tabAsset` a, `tabDepreciation Schedule` ds - where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and a.name = ds.parent and ifnull(ds.journal_entry, '') != '' + from `tabAsset` a, `tabAsset Depreciation Schedule` ads, `tabDepreciation Schedule` ds + where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and ads.asset = a.name and ads.docstatus=1 and ads.name = ds.parent and ifnull(ds.journal_entry, '') != '' group by a.asset_category union SELECT a.asset_category, diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 7e542197407..b8185c929e6 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -76,7 +76,6 @@ frappe.ui.form.on('Asset', { refresh: function(frm) { frappe.ui.form.trigger("Asset", "is_existing_asset"); frm.toggle_display("next_depreciation_date", frm.doc.docstatus < 1); - frm.events.make_schedules_editable(frm); if (frm.doc.docstatus==1) { if (in_list(["Submitted", "Partially Depreciated", "Fully Depreciated"], frm.doc.status)) { @@ -188,7 +187,11 @@ frappe.ui.form.on('Asset', { }) }, - setup_chart: function(frm) { + setup_chart: async 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; @@ -202,7 +205,20 @@ frappe.ui.form.on('Asset', { flt(frm.doc.opening_accumulated_depreciation)); } - $.each(frm.doc.schedules || [], function(i, v) { + let depr_schedule = []; + + if (frm.doc.finance_books.length == 1) { + depr_schedule = (await frappe.call( + "erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule.get_depr_schedule", + { + asset_name: frm.doc.name, + status: frm.doc.docstatus ? "Active" : "Draft", + finance_book: frm.doc.finance_books[0].finance_book || null + } + )).message; + } + + $.each(depr_schedule || [], function(i, v) { x_intervals.push(v.schedule_date); var asset_value = flt(frm.doc.gross_purchase_amount) - flt(v.accumulated_depreciation_amount); if(v.journal_entry) { @@ -266,21 +282,6 @@ frappe.ui.form.on('Asset', { // frm.toggle_reqd("next_depreciation_date", (!frm.doc.is_existing_asset && frm.doc.calculate_depreciation)); }, - opening_accumulated_depreciation: function(frm) { - erpnext.asset.set_accumulated_depreciation(frm); - }, - - make_schedules_editable: function(frm) { - if (frm.doc.finance_books) { - var is_editable = frm.doc.finance_books.filter(d => d.depreciation_method == "Manual").length > 0 - ? true : false; - - frm.toggle_enable("schedules", is_editable); - frm.fields_dict["schedules"].grid.toggle_enable("schedule_date", is_editable); - frm.fields_dict["schedules"].grid.toggle_enable("depreciation_amount", is_editable); - } - }, - make_sales_invoice: function(frm) { frappe.call({ args: { @@ -476,7 +477,6 @@ frappe.ui.form.on('Asset Finance Book', { depreciation_method: function(frm, cdt, cdn) { const row = locals[cdt][cdn]; frm.events.set_depreciation_rate(frm, row); - frm.events.make_schedules_editable(frm); }, expected_value_after_useful_life: function(frm, cdt, cdn) { @@ -512,41 +512,6 @@ frappe.ui.form.on('Asset Finance Book', { } }); -frappe.ui.form.on('Depreciation Schedule', { - make_depreciation_entry: function(frm, cdt, cdn) { - var row = locals[cdt][cdn]; - if (!row.journal_entry) { - frappe.call({ - method: "erpnext.assets.doctype.asset.depreciation.make_depreciation_entry", - args: { - "asset_name": frm.doc.name, - "date": row.schedule_date - }, - callback: function(r) { - frappe.model.sync(r.message); - frm.refresh(); - } - }) - } - }, - - depreciation_amount: function(frm, cdt, cdn) { - erpnext.asset.set_accumulated_depreciation(frm); - } - -}) - -erpnext.asset.set_accumulated_depreciation = function(frm) { - if(frm.doc.depreciation_method != "Manual") return; - - var accumulated_depreciation = flt(frm.doc.opening_accumulated_depreciation); - $.each(frm.doc.schedules || [], function(i, row) { - accumulated_depreciation += flt(row.depreciation_amount); - frappe.model.set_value(row.doctype, row.name, - "accumulated_depreciation_amount", accumulated_depreciation); - }) -}; - erpnext.asset.scrap_asset = function(frm) { frappe.confirm(__("Do you really want to scrap this asset?"), function () { frappe.call({ diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index f0505ff9835..4bac3031e8a 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -52,8 +52,6 @@ "column_break_24", "frequency_of_depreciation", "next_depreciation_date", - "section_break_14", - "schedules", "insurance_details", "policy_number", "insurer", @@ -307,19 +305,6 @@ "label": "Next Depreciation Date", "no_copy": 1 }, - { - "depends_on": "calculate_depreciation", - "fieldname": "section_break_14", - "fieldtype": "Section Break", - "label": "Depreciation Schedule" - }, - { - "fieldname": "schedules", - "fieldtype": "Table", - "label": "Depreciation Schedule", - "no_copy": 1, - "options": "Depreciation Schedule" - }, { "collapsible": 1, "fieldname": "insurance_details", @@ -508,9 +493,14 @@ "group": "Value", "link_doctype": "Asset Value Adjustment", "link_fieldname": "asset" + }, + { + "group": "Depreciation", + "link_doctype": "Asset Depreciation Schedule", + "link_fieldname": "asset" } ], - "modified": "2022-07-20 10:15:12.887372", + "modified": "2022-11-25 12:47:19.689702", "modified_by": "Administrator", "module": "Assets", "name": "Asset", diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index ca6be9b57b2..df05d5e6325 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -8,14 +8,15 @@ import math import frappe from frappe import _ from frappe.utils import ( - add_days, add_months, cint, date_diff, flt, get_datetime, get_last_day, + get_link_to_form, getdate, + is_last_day_of_the_month, month_diff, nowdate, today, @@ -28,6 +29,16 @@ from erpnext.assets.doctype.asset.depreciation import ( get_disposal_account_and_cost_center, ) from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account +from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( + cancel_asset_depr_schedules, + convert_draft_asset_depr_schedules_into_active, + get_asset_depr_schedule_doc, + get_depr_schedule, + make_draft_asset_depr_schedules, + make_draft_asset_depr_schedules_if_not_present, + set_draft_asset_depr_schedule_details, + update_draft_asset_depr_schedules, +) from erpnext.controllers.accounts_controller import AccountsController @@ -40,9 +51,9 @@ class Asset(AccountsController): self.set_missing_values() if not self.split_from: self.prepare_depreciation_data() + update_draft_asset_depr_schedules(self) self.validate_gross_and_purchase_amount() - if self.get("schedules"): - self.validate_expected_value_after_useful_life() + self.validate_expected_value_after_useful_life() self.status = self.get_status() @@ -52,16 +63,24 @@ class Asset(AccountsController): self.make_asset_movement() if not self.booked_fixed_asset and self.validate_make_gl_entry(): self.make_gl_entries() + if not self.split_from: + make_draft_asset_depr_schedules_if_not_present(self) + convert_draft_asset_depr_schedules_into_active(self) def on_cancel(self): self.validate_cancellation() self.cancel_movement_entries() self.delete_depreciation_entries() + cancel_asset_depr_schedules(self) self.set_status() self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") make_reverse_gl_entries(voucher_type="Asset", voucher_no=self.name) self.db_set("booked_fixed_asset", 0) + def after_insert(self): + if not self.split_from: + make_draft_asset_depr_schedules(self) + def validate_asset_and_reference(self): if self.purchase_invoice or self.purchase_receipt: reference_doc = "Purchase Invoice" if self.purchase_invoice else "Purchase Receipt" @@ -79,12 +98,10 @@ class Asset(AccountsController): _("Purchase Invoice cannot be made against an existing asset {0}").format(self.name) ) - def prepare_depreciation_data(self, date_of_disposal=None, date_of_return=None): + def prepare_depreciation_data(self): if self.calculate_depreciation: self.value_after_depreciation = 0 self.set_depreciation_rate() - 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,148 +240,6 @@ class Asset(AccountsController): self.get_depreciation_rate(d, on_validate=True), d.precision("rate_of_depreciation") ) - 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" - ): - self.schedules = [] - - if not self.available_for_use_date: - return - - start = self.clear_depreciation_schedule() - - for finance_book in self.get("finance_books"): - self._make_depreciation_schedule(finance_book, start, date_of_disposal) - - def _make_depreciation_schedule(self, finance_book, start, date_of_disposal): - self.validate_asset_finance_books(finance_book) - - value_after_depreciation = self._get_value_after_depreciation(finance_book) - finance_book.value_after_depreciation = value_after_depreciation - - number_of_pending_depreciations = cint(finance_book.total_number_of_depreciations) - cint( - self.number_of_depreciations_booked - ) - - has_pro_rata = self.check_is_pro_rata(finance_book) - if has_pro_rata: - number_of_pending_depreciations += 1 - - skip_row = False - should_get_last_day = is_last_day_of_the_month(finance_book.depreciation_start_date) - - for n in range(start[finance_book.idx - 1], number_of_pending_depreciations): - # If depreciation is already completed (for double declining balance) - if skip_row: - continue - - depreciation_amount = get_depreciation_amount(self, value_after_depreciation, finance_book) - - if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1: - schedule_date = add_months( - finance_book.depreciation_start_date, n * cint(finance_book.frequency_of_depreciation) - ) - - if should_get_last_day: - schedule_date = get_last_day(schedule_date) - - # schedule date will be a year later from start date - # so monthly schedule date is calculated by removing 11 months from it - monthly_schedule_date = add_months(schedule_date, -finance_book.frequency_of_depreciation + 1) - - # if asset is being sold - 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_disposal - ) - - if depreciation_amount > 0: - self._add_depreciation_row( - date_of_disposal, - depreciation_amount, - finance_book.depreciation_method, - finance_book.finance_book, - finance_book.idx, - ) - - break - - # For first row - if has_pro_rata and not self.opening_accumulated_depreciation and n == 0: - from_date = add_days( - self.available_for_use_date, -1 - ) # needed to calc depr amount for available_for_use_date too - depreciation_amount, days, months = self.get_pro_rata_amt( - finance_book, depreciation_amount, from_date, finance_book.depreciation_start_date - ) - - # For first depr schedule date will be the start date - # so monthly schedule date is calculated by removing month difference between use date and start date - monthly_schedule_date = add_months(finance_book.depreciation_start_date, -months + 1) - - # For last row - elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1: - if not self.flags.increase_in_asset_life: - # In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission - self.to_date = add_months( - self.available_for_use_date, - (n + self.number_of_depreciations_booked) * cint(finance_book.frequency_of_depreciation), - ) - - depreciation_amount_without_pro_rata = depreciation_amount - - depreciation_amount, days, months = self.get_pro_rata_amt( - finance_book, depreciation_amount, schedule_date, self.to_date - ) - - depreciation_amount = self.get_adjusted_depreciation_amount( - depreciation_amount_without_pro_rata, depreciation_amount, finance_book.finance_book - ) - - monthly_schedule_date = add_months(schedule_date, 1) - schedule_date = add_days(schedule_date, days) - last_schedule_date = schedule_date - - if not depreciation_amount: - continue - value_after_depreciation -= flt(depreciation_amount, self.precision("gross_purchase_amount")) - - # Adjust depreciation amount in the last period based on the expected value after useful life - if finance_book.expected_value_after_useful_life and ( - ( - n == cint(number_of_pending_depreciations) - 1 - and value_after_depreciation != finance_book.expected_value_after_useful_life - ) - or value_after_depreciation < finance_book.expected_value_after_useful_life - ): - depreciation_amount += value_after_depreciation - finance_book.expected_value_after_useful_life - skip_row = True - - if depreciation_amount > 0: - self._add_depreciation_row( - schedule_date, - depreciation_amount, - finance_book.depreciation_method, - finance_book.finance_book, - finance_book.idx, - ) - - def _add_depreciation_row( - self, schedule_date, depreciation_amount, depreciation_method, finance_book, finance_book_id - ): - self.append( - "schedules", - { - "schedule_date": schedule_date, - "depreciation_amount": depreciation_amount, - "depreciation_method": depreciation_method, - "finance_book": finance_book, - "finance_book_id": finance_book_id, - }, - ) - def _get_value_after_depreciation(self, finance_book): # value_after_depreciation - current Asset value if self.docstatus == 1 and finance_book.value_after_depreciation: @@ -376,58 +251,6 @@ class Asset(AccountsController): return value_after_depreciation - # 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): - start = [] - num_of_depreciations_completed = 0 - depr_schedule = [] - - for schedule in self.get("schedules"): - # to update start when there are JEs linked with all the schedule rows corresponding to an FB - if len(start) == (int(schedule.finance_book_id) - 2): - start.append(num_of_depreciations_completed) - num_of_depreciations_completed = 0 - - # to ensure that start will only be updated once for each FB - if len(start) == (int(schedule.finance_book_id) - 1): - if schedule.journal_entry: - num_of_depreciations_completed += 1 - depr_schedule.append(schedule) - else: - start.append(num_of_depreciations_completed) - num_of_depreciations_completed = 0 - - # to update start when all the schedule rows corresponding to the last FB are linked with JEs - if len(start) == (len(self.finance_books) - 1): - start.append(num_of_depreciations_completed) - - # when the Depreciation Schedule is being created for the first time - if start == []: - start = [0] * len(self.finance_books) - else: - self.schedules = depr_schedule - - return start - - def get_from_date(self, finance_book): - if not self.get("schedules"): - return self.available_for_use_date - - if len(self.finance_books) == 1: - return self.schedules[-1].schedule_date - - from_date = "" - for schedule in self.get("schedules"): - if schedule.finance_book == finance_book: - from_date = schedule.schedule_date - - if from_date: - return from_date - - # since depr for available_for_use_date is not yet booked - return add_days(self.available_for_use_date, -1) - # if it returns True, depreciation_amount will not be equal for the first and last rows def check_is_pro_rata(self, row): has_pro_rata = False @@ -512,83 +335,15 @@ class Asset(AccountsController): ).format(row.idx) ) - # to ensure that final accumulated depreciation amount is accurate - def get_adjusted_depreciation_amount( - self, depreciation_amount_without_pro_rata, depreciation_amount_for_last_row, finance_book - ): - if not self.opening_accumulated_depreciation: - depreciation_amount_for_first_row = self.get_depreciation_amount_for_first_row(finance_book) - - if ( - depreciation_amount_for_first_row + depreciation_amount_for_last_row - != depreciation_amount_without_pro_rata - ): - depreciation_amount_for_last_row = ( - depreciation_amount_without_pro_rata - depreciation_amount_for_first_row - ) - - return depreciation_amount_for_last_row - - def get_depreciation_amount_for_first_row(self, finance_book): - if self.has_only_one_finance_book(): - return self.schedules[0].depreciation_amount - else: - for schedule in self.schedules: - if schedule.finance_book == finance_book: - return schedule.depreciation_amount - - def has_only_one_finance_book(self): - if len(self.finance_books) == 1: - return True - - def set_accumulated_depreciation( - self, date_of_sale=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" - ] - finance_books = [] - - for i, d in enumerate(self.get("schedules")): - if ignore_booked_entry and d.journal_entry: - continue - - if int(d.finance_book_id) not in finance_books: - accumulated_depreciation = flt(self.opening_accumulated_depreciation) - value_after_depreciation = flt(self.get_value_after_depreciation(d.finance_book_id)) - finance_books.append(int(d.finance_book_id)) - - depreciation_amount = flt(d.depreciation_amount, d.precision("depreciation_amount")) - value_after_depreciation -= flt(depreciation_amount) - - # for the last row, if depreciation method = Straight Line - if ( - straight_line_idx - and i == max(straight_line_idx) - 1 - and not date_of_sale - and not date_of_return - ): - book = self.get("finance_books")[cint(d.finance_book_id) - 1] - depreciation_amount += flt( - value_after_depreciation - flt(book.expected_value_after_useful_life), - d.precision("depreciation_amount"), - ) - - d.depreciation_amount = depreciation_amount - accumulated_depreciation += d.depreciation_amount - d.accumulated_depreciation_amount = flt( - accumulated_depreciation, d.precision("accumulated_depreciation_amount") - ) - - def get_value_after_depreciation(self, idx): - return flt(self.get("finance_books")[cint(idx) - 1].value_after_depreciation) - def validate_expected_value_after_useful_life(self): for row in self.get("finance_books"): + depr_schedule = get_depr_schedule(self.name, "Draft", row.finance_book) + + if not depr_schedule: + continue + accumulated_depreciation_after_full_schedule = [ - d.accumulated_depreciation_amount - for d in self.get("schedules") - if cint(d.finance_book_id) == row.idx + d.accumulated_depreciation_amount for d in depr_schedule ] if accumulated_depreciation_after_full_schedule: @@ -637,10 +392,13 @@ class Asset(AccountsController): movement.cancel() def delete_depreciation_entries(self): - for d in self.get("schedules"): - if d.journal_entry: - frappe.get_doc("Journal Entry", d.journal_entry).cancel() - d.db_set("journal_entry", None) + for row in self.get("finance_books"): + depr_schedule = get_depr_schedule(self.name, "Active", row.finance_book) + + for d in depr_schedule or []: + if d.journal_entry: + frappe.get_doc("Journal Entry", d.journal_entry).cancel() + d.db_set("journal_entry", None) self.db_set( "value_after_depreciation", @@ -1072,32 +830,6 @@ def get_total_days(date, frequency): return date_diff(date, period_start_date) -def is_last_day_of_the_month(date): - last_day_of_the_month = get_last_day(date) - - return getdate(last_day_of_the_month) == getdate(date) - - -@erpnext.allow_regional -def get_depreciation_amount(asset, depreciable_value, row): - if row.depreciation_method in ("Straight Line", "Manual"): - # if the Depreciation Schedule is being prepared for the first time - if not asset.flags.increase_in_asset_life: - depreciation_amount = ( - flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life) - ) / flt(row.total_number_of_depreciations) - - # if the Depreciation Schedule is being modified after Asset Repair - else: - depreciation_amount = ( - flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life) - ) / (date_diff(asset.to_date, asset.available_for_use_date) / 365) - else: - depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100)) - - return depreciation_amount - - @frappe.whitelist() def split_asset(asset_name, split_qty): asset = frappe.get_doc("Asset", asset_name) @@ -1109,12 +841,12 @@ def split_asset(asset_name, split_qty): remaining_qty = asset.asset_quantity - split_qty new_asset = create_new_asset_after_split(asset, split_qty) - update_existing_asset(asset, remaining_qty) + update_existing_asset(asset, remaining_qty, new_asset.name) return new_asset -def update_existing_asset(asset, remaining_qty): +def update_existing_asset(asset, remaining_qty, new_asset_name): remaining_gross_purchase_amount = flt( (asset.gross_purchase_amount * remaining_qty) / asset.asset_quantity ) @@ -1132,34 +864,49 @@ def update_existing_asset(asset, remaining_qty): }, ) - for finance_book in asset.get("finance_books"): + for row in asset.get("finance_books"): value_after_depreciation = flt( - (finance_book.value_after_depreciation * remaining_qty) / asset.asset_quantity + (row.value_after_depreciation * remaining_qty) / asset.asset_quantity ) expected_value_after_useful_life = flt( - (finance_book.expected_value_after_useful_life * remaining_qty) / asset.asset_quantity + (row.expected_value_after_useful_life * remaining_qty) / asset.asset_quantity ) frappe.db.set_value( - "Asset Finance Book", finance_book.name, "value_after_depreciation", value_after_depreciation + "Asset Finance Book", row.name, "value_after_depreciation", value_after_depreciation ) frappe.db.set_value( "Asset Finance Book", - finance_book.name, + row.name, "expected_value_after_useful_life", expected_value_after_useful_life, ) - accumulated_depreciation = 0 + current_asset_depr_schedule_doc = get_asset_depr_schedule_doc( + asset.name, "Active", row.finance_book + ) + new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc) - for term in asset.get("schedules"): - depreciation_amount = flt((term.depreciation_amount * remaining_qty) / asset.asset_quantity) - frappe.db.set_value( - "Depreciation Schedule", term.name, "depreciation_amount", depreciation_amount - ) - accumulated_depreciation += depreciation_amount - frappe.db.set_value( - "Depreciation Schedule", term.name, "accumulated_depreciation_amount", accumulated_depreciation + set_draft_asset_depr_schedule_details(new_asset_depr_schedule_doc, asset, row) + + accumulated_depreciation = 0 + + for term in new_asset_depr_schedule_doc.get("depreciation_schedule"): + depreciation_amount = flt((term.depreciation_amount * remaining_qty) / asset.asset_quantity) + term.depreciation_amount = depreciation_amount + accumulated_depreciation += depreciation_amount + term.accumulated_depreciation_amount = accumulated_depreciation + + notes = _( + "This schedule was created when Asset {0} was updated after being split into new Asset {1}." + ).format( + get_link_to_form(asset.doctype, asset.name), get_link_to_form(asset.doctype, new_asset_name) ) + new_asset_depr_schedule_doc.notes = notes + + current_asset_depr_schedule_doc.flags.should_not_cancel_depreciation_entries = True + current_asset_depr_schedule_doc.cancel() + + new_asset_depr_schedule_doc.submit() def create_new_asset_after_split(asset, split_qty): @@ -1173,31 +920,49 @@ def create_new_asset_after_split(asset, split_qty): new_asset.opening_accumulated_depreciation = opening_accumulated_depreciation new_asset.asset_quantity = split_qty new_asset.split_from = asset.name - accumulated_depreciation = 0 - for finance_book in new_asset.get("finance_books"): - finance_book.value_after_depreciation = flt( - (finance_book.value_after_depreciation * split_qty) / asset.asset_quantity + for row in new_asset.get("finance_books"): + row.value_after_depreciation = flt( + (row.value_after_depreciation * split_qty) / asset.asset_quantity ) - finance_book.expected_value_after_useful_life = flt( - (finance_book.expected_value_after_useful_life * split_qty) / asset.asset_quantity + row.expected_value_after_useful_life = flt( + (row.expected_value_after_useful_life * split_qty) / asset.asset_quantity ) - for term in new_asset.get("schedules"): - depreciation_amount = flt((term.depreciation_amount * split_qty) / asset.asset_quantity) - term.depreciation_amount = depreciation_amount - accumulated_depreciation += depreciation_amount - term.accumulated_depreciation_amount = accumulated_depreciation - new_asset.submit() new_asset.set_status() - for term in new_asset.get("schedules"): - # Update references in JV - if term.journal_entry: - add_reference_in_jv_on_split( - term.journal_entry, new_asset.name, asset.name, term.depreciation_amount - ) + for row in new_asset.get("finance_books"): + current_asset_depr_schedule_doc = get_asset_depr_schedule_doc( + asset.name, "Active", row.finance_book + ) + new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc) + + set_draft_asset_depr_schedule_details(new_asset_depr_schedule_doc, new_asset, row) + + accumulated_depreciation = 0 + + for term in new_asset_depr_schedule_doc.get("depreciation_schedule"): + depreciation_amount = flt((term.depreciation_amount * split_qty) / asset.asset_quantity) + term.depreciation_amount = depreciation_amount + accumulated_depreciation += depreciation_amount + term.accumulated_depreciation_amount = accumulated_depreciation + + notes = _("This schedule was created when new Asset {0} was split from Asset {1}.").format( + get_link_to_form(new_asset.doctype, new_asset.name), get_link_to_form(asset.doctype, asset.name) + ) + new_asset_depr_schedule_doc.notes = notes + + new_asset_depr_schedule_doc.submit() + + for row in new_asset.get("finance_books"): + depr_schedule = get_depr_schedule(new_asset.name, "Active", row.finance_book) + for term in depr_schedule: + # Update references in JV + if term.journal_entry: + add_reference_in_jv_on_split( + term.journal_entry, new_asset.name, asset.name, term.depreciation_amount + ) return new_asset diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index 97941706aa8..7686c348a63 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -4,12 +4,18 @@ import frappe from frappe import _ -from frappe.utils import add_months, cint, flt, getdate, nowdate, today +from frappe.utils import add_months, cint, flt, get_link_to_form, getdate, nowdate, today from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_checks_for_pl_and_bs_accounts, ) from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry +from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( + get_asset_depr_schedule_doc, + get_asset_depr_schedule_name, + get_temp_asset_depr_schedule_doc, + make_new_active_asset_depr_schedules_and_cancel_current_ones, +) def post_depreciation_entries(date=None, commit=True): @@ -21,8 +27,11 @@ def post_depreciation_entries(date=None, commit=True): if not date: date = today() - for asset in get_depreciable_assets(date): - make_depreciation_entry(asset, date) + for asset_name in get_depreciable_assets(date): + asset_doc = frappe.get_doc("Asset", asset_name) + + make_depreciation_entry_for_all_asset_depr_schedules(asset_doc, date) + if commit: frappe.db.commit() @@ -30,21 +39,35 @@ def post_depreciation_entries(date=None, commit=True): def get_depreciable_assets(date): return frappe.db.sql_list( """select distinct a.name - from tabAsset a, `tabDepreciation Schedule` ds - where a.name = ds.parent and a.docstatus=1 and ds.schedule_date<=%s and a.calculate_depreciation = 1 + from tabAsset a, `tabAsset Depreciation Schedule` ads, `tabDepreciation Schedule` ds + where a.name = ads.asset and ads.name = ds.parent and a.docstatus=1 and ads.docstatus=1 and a.status in ('Submitted', 'Partially Depreciated') + and a.calculate_depreciation = 1 + and ds.schedule_date<=%s and ifnull(ds.journal_entry, '')=''""", date, ) +def make_depreciation_entry_for_all_asset_depr_schedules(asset_doc, date=None): + for row in asset_doc.get("finance_books"): + asset_depr_schedule_name = get_asset_depr_schedule_name( + asset_doc.name, "Active", row.finance_book + ) + make_depreciation_entry(asset_depr_schedule_name, date) + + @frappe.whitelist() -def make_depreciation_entry(asset_name, date=None): +def make_depreciation_entry(asset_depr_schedule_name, date=None): frappe.has_permission("Journal Entry", throw=True) if not date: date = today() + asset_depr_schedule_doc = frappe.get_doc("Asset Depreciation Schedule", asset_depr_schedule_name) + + asset_name = asset_depr_schedule_doc.asset + asset = frappe.get_doc("Asset", asset_name) ( fixed_asset_account, @@ -60,14 +83,14 @@ def make_depreciation_entry(asset_name, date=None): accounting_dimensions = get_checks_for_pl_and_bs_accounts() - for d in asset.get("schedules"): + for d in asset_depr_schedule_doc.get("depreciation_schedule"): if not d.journal_entry and getdate(d.schedule_date) <= getdate(date): je = frappe.new_doc("Journal Entry") je.voucher_type = "Depreciation Entry" je.naming_series = depreciation_series je.posting_date = d.schedule_date je.company = asset.company - je.finance_book = d.finance_book + je.finance_book = asset_depr_schedule_doc.finance_book je.remark = "Depreciation Entry against {0} worth {1}".format(asset_name, d.depreciation_amount) credit_account, debit_account = get_credit_and_debit_accounts( @@ -118,14 +141,14 @@ def make_depreciation_entry(asset_name, date=None): d.db_set("journal_entry", je.name) - idx = cint(d.finance_book_id) - finance_books = asset.get("finance_books")[idx - 1] - finance_books.value_after_depreciation -= d.depreciation_amount - finance_books.db_update() + idx = cint(asset_depr_schedule_doc.finance_book_id) + row = asset.get("finance_books")[idx - 1] + row.value_after_depreciation -= d.depreciation_amount + row.db_update() asset.set_status() - return asset + return asset_depr_schedule_doc def get_depreciation_accounts(asset): @@ -199,7 +222,11 @@ def scrap_asset(asset_name): date = today() - depreciate_asset(asset, date) + notes = _("This schedule was created when Asset {0} was scrapped.").format( + get_link_to_form(asset.doctype, asset.name) + ) + + depreciate_asset(asset, date, notes) asset.reload() depreciation_series = frappe.get_cached_value( @@ -232,10 +259,15 @@ 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 + notes = _("This schedule was created when Asset {0} was restored.").format( + get_link_to_form(asset.doctype, asset.name) + ) + + reset_depreciation_schedule(asset, asset.disposal_date, notes) + asset.db_set("disposal_date", None) asset.db_set("journal_entry_for_scrap", None) @@ -244,22 +276,28 @@ 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() +def depreciate_asset(asset_doc, date, notes): + asset_doc.flags.ignore_validate_update_after_submit = True - make_depreciation_entry(asset.name, date) + make_new_active_asset_depr_schedules_and_cancel_current_ones( + asset_doc, notes, date_of_disposal=date + ) + + asset_doc.save() + + make_depreciation_entry_for_all_asset_depr_schedules(asset_doc, date) -def reset_depreciation_schedule(asset, date): - asset.flags.ignore_validate_update_after_submit = True +def reset_depreciation_schedule(asset_doc, date, notes): + asset_doc.flags.ignore_validate_update_after_submit = True - # recreate original depreciation schedule of the asset - asset.prepare_depreciation_data(date_of_return=date) + make_new_active_asset_depr_schedules_and_cancel_current_ones( + asset_doc, notes, date_of_return=date + ) - modify_depreciation_schedule_for_asset_repairs(asset) - asset.save() + modify_depreciation_schedule_for_asset_repairs(asset_doc) + + asset_doc.save() def modify_depreciation_schedule_for_asset_repairs(asset): @@ -271,35 +309,36 @@ def modify_depreciation_schedule_for_asset_repairs(asset): if repair.increase_in_asset_life: asset_repair = frappe.get_doc("Asset Repair", repair.name) asset_repair.modify_depreciation_schedule() - asset.prepare_depreciation_data() + notes = _("This schedule was created when Asset {0} went through Asset Repair {1}.").format( + get_link_to_form(asset.doctype, asset.name), + get_link_to_form(asset_repair.doctype, asset_repair.name), + ) + make_new_active_asset_depr_schedules_and_cancel_current_ones(asset, notes) def reverse_depreciation_entry_made_after_disposal(asset, date): - 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 + for row in asset.get("finance_books"): + asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active", row.finance_book) - 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): + for schedule_idx, schedule in enumerate(asset_depr_schedule_doc.get("depreciation_schedule")): + if schedule.schedule_date == date: + if not disposal_was_made_on_original_schedule_date( + schedule_idx, 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() + 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) - asset.finance_books[0].value_after_depreciation += depreciation_amount - asset.save() + frappe.flags.is_reverse_depr_entry = False + asset_depr_schedule_doc.flags.ignore_validate_update_after_submit = True + asset.flags.ignore_validate_update_after_submit = True + schedule.journal_entry = None + depreciation_amount = get_depreciation_amount_in_je(reverse_journal_entry) + row.value_after_depreciation += depreciation_amount + asset_depr_schedule_doc.save() + asset.save() def get_depreciation_amount_in_je(journal_entry): @@ -310,15 +349,14 @@ def get_depreciation_amount_in_je(journal_entry): # 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) - ) +def disposal_was_made_on_original_schedule_date(schedule_idx, row, posting_date_of_disposal): + orginal_schedule_date = add_months( + row.depreciation_start_date, schedule_idx * cint(row.frequency_of_depreciation) + ) + + if orginal_schedule_date == posting_date_of_disposal: + return True - if orginal_schedule_date == posting_date_of_disposal: - return True return False @@ -499,24 +537,27 @@ def get_disposal_account_and_cost_center(company): def get_value_after_depreciation_on_disposal_date(asset, disposal_date, finance_book=None): asset_doc = frappe.get_doc("Asset", asset) - if asset_doc.calculate_depreciation: - asset_doc.prepare_depreciation_data(getdate(disposal_date)) - - finance_book_id = 1 - if finance_book: - for fb in asset_doc.finance_books: - if fb.finance_book == finance_book: - finance_book_id = fb.idx - break - - asset_schedules = [ - sch for sch in asset_doc.schedules if cint(sch.finance_book_id) == finance_book_id - ] - accumulated_depr_amount = asset_schedules[-1].accumulated_depreciation_amount - - return flt( - flt(asset_doc.gross_purchase_amount) - accumulated_depr_amount, - asset_doc.precision("gross_purchase_amount"), - ) - else: + if not asset_doc.calculate_depreciation: return flt(asset_doc.value_after_depreciation) + + idx = 1 + if finance_book: + for d in asset.finance_books: + if d.finance_book == finance_book: + idx = d.idx + break + + row = asset_doc.finance_books[idx - 1] + + temp_asset_depreciation_schedule = get_temp_asset_depr_schedule_doc( + asset_doc, row, getdate(disposal_date) + ) + + accumulated_depr_amount = temp_asset_depreciation_schedule.get("depreciation_schedule")[ + -1 + ].accumulated_depreciation_amount + + return flt( + flt(asset_doc.gross_purchase_amount) - accumulated_depr_amount, + asset_doc.precision("gross_purchase_amount"), + ) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 2bec27371b5..d61ef8ecf8a 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -27,6 +27,11 @@ from erpnext.assets.doctype.asset.depreciation import ( restore_asset, scrap_asset, ) +from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( + clear_depr_schedule, + get_asset_depr_schedule_doc, + get_depr_schedule, +) from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( make_purchase_invoice as make_invoice, ) @@ -205,6 +210,9 @@ class TestAsset(AssetSetup): submit=1, ) + first_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active") + self.assertEquals(first_asset_depr_schedule.status, "Active") + post_depreciation_entries(date=add_months(purchase_date, 2)) asset.load_from_db() @@ -216,6 +224,11 @@ class TestAsset(AssetSetup): scrap_asset(asset.name) asset.load_from_db() + first_asset_depr_schedule.load_from_db() + + second_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active") + self.assertEquals(second_asset_depr_schedule.status, "Active") + self.assertEquals(first_asset_depr_schedule.status, "Cancelled") accumulated_depr_amount = flt( asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation, @@ -256,6 +269,11 @@ class TestAsset(AssetSetup): self.assertSequenceEqual(gle, expected_gle) restore_asset(asset.name) + second_asset_depr_schedule.load_from_db() + + third_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active") + self.assertEquals(third_asset_depr_schedule.status, "Active") + self.assertEquals(second_asset_depr_schedule.status, "Cancelled") asset.load_from_db() self.assertFalse(asset.journal_entry_for_scrap) @@ -283,6 +301,9 @@ class TestAsset(AssetSetup): submit=1, ) + first_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active") + self.assertEquals(first_asset_depr_schedule.status, "Active") + post_depreciation_entries(date=add_months(purchase_date, 2)) si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company") @@ -294,6 +315,12 @@ class TestAsset(AssetSetup): self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold") + first_asset_depr_schedule.load_from_db() + + second_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active") + self.assertEquals(second_asset_depr_schedule.status, "Active") + self.assertEquals(first_asset_depr_schedule.status, "Cancelled") + pro_rata_amount, _, _ = asset.get_pro_rata_amt( asset.finance_books[0], 9000, get_last_day(add_months(purchase_date, 1)), date ) @@ -370,6 +397,9 @@ class TestAsset(AssetSetup): submit=1, ) + first_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active") + self.assertEquals(first_asset_depr_schedule.status, "Active") + post_depreciation_entries(date="2021-01-01") self.assertEqual(asset.asset_quantity, 10) @@ -378,21 +408,31 @@ class TestAsset(AssetSetup): new_asset = split_asset(asset.name, 2) asset.load_from_db() + first_asset_depr_schedule.load_from_db() + + second_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active") + first_asset_depr_schedule_of_new_asset = get_asset_depr_schedule_doc(new_asset.name, "Active") + self.assertEquals(second_asset_depr_schedule.status, "Active") + self.assertEquals(first_asset_depr_schedule_of_new_asset.status, "Active") + self.assertEquals(first_asset_depr_schedule.status, "Cancelled") + + depr_schedule_of_asset = second_asset_depr_schedule.get("depreciation_schedule") + depr_schedule_of_new_asset = first_asset_depr_schedule_of_new_asset.get("depreciation_schedule") self.assertEqual(new_asset.asset_quantity, 2) self.assertEqual(new_asset.gross_purchase_amount, 24000) self.assertEqual(new_asset.opening_accumulated_depreciation, 4000) self.assertEqual(new_asset.split_from, asset.name) - self.assertEqual(new_asset.schedules[0].depreciation_amount, 4000) - self.assertEqual(new_asset.schedules[1].depreciation_amount, 4000) + self.assertEqual(depr_schedule_of_new_asset[0].depreciation_amount, 4000) + self.assertEqual(depr_schedule_of_new_asset[1].depreciation_amount, 4000) self.assertEqual(asset.asset_quantity, 8) self.assertEqual(asset.gross_purchase_amount, 96000) self.assertEqual(asset.opening_accumulated_depreciation, 16000) - self.assertEqual(asset.schedules[0].depreciation_amount, 16000) - self.assertEqual(asset.schedules[1].depreciation_amount, 16000) + self.assertEqual(depr_schedule_of_asset[0].depreciation_amount, 16000) + self.assertEqual(depr_schedule_of_asset[1].depreciation_amount, 16000) - journal_entry = asset.schedules[0].journal_entry + journal_entry = depr_schedule_of_asset[0].journal_entry jv = frappe.get_doc("Journal Entry", journal_entry) self.assertEqual(jv.accounts[0].credit_in_account_currency, 16000) @@ -629,7 +669,7 @@ class TestDepreciationMethods(AssetSetup): schedules = [ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] - for d in asset.get("schedules") + for d in get_depr_schedule(asset.name, "Draft") ] self.assertEqual(schedules, expected_schedules) @@ -651,7 +691,7 @@ class TestDepreciationMethods(AssetSetup): expected_schedules = [["2032-12-31", 30000.0, 77095.89], ["2033-06-06", 12904.11, 90000.0]] schedules = [ [cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount] - for d in asset.get("schedules") + for d in get_depr_schedule(asset.name, "Draft") ] self.assertEqual(schedules, expected_schedules) @@ -678,7 +718,7 @@ class TestDepreciationMethods(AssetSetup): schedules = [ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] - for d in asset.get("schedules") + for d in get_depr_schedule(asset.name, "Draft") ] self.assertEqual(schedules, expected_schedules) @@ -703,7 +743,7 @@ class TestDepreciationMethods(AssetSetup): schedules = [ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] - for d in asset.get("schedules") + for d in get_depr_schedule(asset.name, "Draft") ] self.assertEqual(schedules, expected_schedules) @@ -733,7 +773,7 @@ class TestDepreciationMethods(AssetSetup): flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2), ] - for d in asset.get("schedules") + for d in get_depr_schedule(asset.name, "Draft") ] self.assertEqual(schedules, expected_schedules) @@ -765,7 +805,7 @@ class TestDepreciationMethods(AssetSetup): flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2), ] - for d in asset.get("schedules") + for d in get_depr_schedule(asset.name, "Draft") ] self.assertEqual(schedules, expected_schedules) @@ -798,7 +838,7 @@ class TestDepreciationMethods(AssetSetup): flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2), ] - for d in asset.get("schedules") + for d in get_depr_schedule(asset.name, "Draft") ] self.assertEqual(schedules, expected_schedules) @@ -831,7 +871,7 @@ class TestDepreciationMethods(AssetSetup): flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2), ] - for d in asset.get("schedules") + for d in get_depr_schedule(asset.name, "Draft") ] self.assertEqual(schedules, expected_schedules) @@ -854,7 +894,7 @@ class TestDepreciationBasics(AssetSetup): ["2022-12-31", 30000, 90000], ] - for i, schedule in enumerate(asset.schedules): + for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")): self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date) self.assertEqual(expected_values[i][1], schedule.depreciation_amount) self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount) @@ -877,7 +917,7 @@ class TestDepreciationBasics(AssetSetup): ["2023-01-01", 15000, 90000], ] - for i, schedule in enumerate(asset.schedules): + for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")): self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date) self.assertEqual(expected_values[i][1], schedule.depreciation_amount) self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount) @@ -885,7 +925,9 @@ class TestDepreciationBasics(AssetSetup): def test_get_depreciation_amount(self): """Tests if get_depreciation_amount() returns the right value.""" - from erpnext.assets.doctype.asset.asset import get_depreciation_amount + from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( + get_depreciation_amount, + ) asset = create_asset(item_code="Macbook Pro", available_for_use_date="2019-12-31") @@ -904,8 +946,8 @@ class TestDepreciationBasics(AssetSetup): depreciation_amount = get_depreciation_amount(asset, 100000, asset.finance_books[0]) self.assertEqual(depreciation_amount, 30000) - def test_make_depreciation_schedule(self): - """Tests if make_depreciation_schedule() returns the right values.""" + def test_make_depr_schedule(self): + """Tests if make_depr_schedule() returns the right values.""" asset = create_asset( item_code="Macbook Pro", @@ -920,7 +962,7 @@ class TestDepreciationBasics(AssetSetup): expected_values = [["2020-12-31", 30000.0], ["2021-12-31", 30000.0], ["2022-12-31", 30000.0]] - for i, schedule in enumerate(asset.schedules): + for i, schedule in enumerate(get_depr_schedule(asset.name, "Draft")): self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date) self.assertEqual(expected_values[i][1], schedule.depreciation_amount) @@ -940,7 +982,7 @@ class TestDepreciationBasics(AssetSetup): expected_values = [30000.0, 60000.0, 90000.0] - for i, schedule in enumerate(asset.schedules): + for i, schedule in enumerate(get_depr_schedule(asset.name, "Draft")): self.assertEqual(expected_values[i], schedule.accumulated_depreciation_amount) def test_check_is_pro_rata(self): @@ -1120,9 +1162,11 @@ class TestDepreciationBasics(AssetSetup): post_depreciation_entries(date="2021-06-01") asset.load_from_db() - self.assertTrue(asset.schedules[0].journal_entry) - self.assertFalse(asset.schedules[1].journal_entry) - self.assertFalse(asset.schedules[2].journal_entry) + depr_schedule = get_depr_schedule(asset.name, "Active") + + self.assertTrue(depr_schedule[0].journal_entry) + self.assertFalse(depr_schedule[1].journal_entry) + self.assertFalse(depr_schedule[2].journal_entry) def test_depr_entry_posting_when_depr_expense_account_is_an_expense_account(self): """Tests if the Depreciation Expense Account gets debited and the Accumulated Depreciation Account gets credited when the former's an Expense Account.""" @@ -1141,7 +1185,7 @@ class TestDepreciationBasics(AssetSetup): post_depreciation_entries(date="2021-06-01") asset.load_from_db() - je = frappe.get_doc("Journal Entry", asset.schedules[0].journal_entry) + je = frappe.get_doc("Journal Entry", get_depr_schedule(asset.name, "Active")[0].journal_entry) accounting_entries = [ {"account": entry.account, "debit": entry.debit, "credit": entry.credit} for entry in je.accounts @@ -1177,7 +1221,7 @@ class TestDepreciationBasics(AssetSetup): post_depreciation_entries(date="2021-06-01") asset.load_from_db() - je = frappe.get_doc("Journal Entry", asset.schedules[0].journal_entry) + je = frappe.get_doc("Journal Entry", get_depr_schedule(asset.name, "Active")[0].journal_entry) accounting_entries = [ {"account": entry.account, "debit": entry.debit, "credit": entry.credit} for entry in je.accounts @@ -1196,8 +1240,8 @@ class TestDepreciationBasics(AssetSetup): depr_expense_account.parent_account = "Expenses - _TC" depr_expense_account.save() - def test_clear_depreciation_schedule(self): - """Tests if clear_depreciation_schedule() works as expected.""" + def test_clear_depr_schedule(self): + """Tests if clear_depr_schedule() works as expected.""" asset = create_asset( item_code="Macbook Pro", @@ -1213,17 +1257,20 @@ class TestDepreciationBasics(AssetSetup): post_depreciation_entries(date="2021-06-01") asset.load_from_db() - asset.clear_depreciation_schedule() + asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active") - self.assertEqual(len(asset.schedules), 1) + clear_depr_schedule(asset_depr_schedule_doc) - def test_clear_depreciation_schedule_for_multiple_finance_books(self): + self.assertEqual(len(asset_depr_schedule_doc.get("depreciation_schedule")), 1) + + def test_clear_depr_schedule_for_multiple_finance_books(self): asset = create_asset(item_code="Macbook Pro", available_for_use_date="2019-12-31", do_not_save=1) asset.calculate_depreciation = 1 asset.append( "finance_books", { + "finance_book": "Test Finance Book 1", "depreciation_method": "Straight Line", "frequency_of_depreciation": 1, "total_number_of_depreciations": 3, @@ -1234,6 +1281,7 @@ class TestDepreciationBasics(AssetSetup): asset.append( "finance_books", { + "finance_book": "Test Finance Book 2", "depreciation_method": "Straight Line", "frequency_of_depreciation": 1, "total_number_of_depreciations": 6, @@ -1244,6 +1292,7 @@ class TestDepreciationBasics(AssetSetup): asset.append( "finance_books", { + "finance_book": "Test Finance Book 3", "depreciation_method": "Straight Line", "frequency_of_depreciation": 12, "total_number_of_depreciations": 3, @@ -1256,15 +1305,23 @@ class TestDepreciationBasics(AssetSetup): post_depreciation_entries(date="2020-04-01") asset.load_from_db() - asset.clear_depreciation_schedule() + asset_depr_schedule_doc_1 = get_asset_depr_schedule_doc( + asset.name, "Active", "Test Finance Book 1" + ) + clear_depr_schedule(asset_depr_schedule_doc_1) + self.assertEqual(len(asset_depr_schedule_doc_1.get("depreciation_schedule")), 3) - self.assertEqual(len(asset.schedules), 6) + asset_depr_schedule_doc_2 = get_asset_depr_schedule_doc( + asset.name, "Active", "Test Finance Book 2" + ) + clear_depr_schedule(asset_depr_schedule_doc_2) + self.assertEqual(len(asset_depr_schedule_doc_2.get("depreciation_schedule")), 3) - for schedule in asset.schedules: - if schedule.idx <= 3: - self.assertEqual(schedule.finance_book_id, "1") - else: - self.assertEqual(schedule.finance_book_id, "2") + asset_depr_schedule_doc_3 = get_asset_depr_schedule_doc( + asset.name, "Active", "Test Finance Book 3" + ) + clear_depr_schedule(asset_depr_schedule_doc_3) + self.assertEqual(len(asset_depr_schedule_doc_3.get("depreciation_schedule")), 0) def test_depreciation_schedules_are_set_up_for_multiple_finance_books(self): asset = create_asset(item_code="Macbook Pro", available_for_use_date="2019-12-31", do_not_save=1) @@ -1273,6 +1330,7 @@ class TestDepreciationBasics(AssetSetup): asset.append( "finance_books", { + "finance_book": "Test Finance Book 1", "depreciation_method": "Straight Line", "frequency_of_depreciation": 12, "total_number_of_depreciations": 3, @@ -1283,6 +1341,7 @@ class TestDepreciationBasics(AssetSetup): asset.append( "finance_books", { + "finance_book": "Test Finance Book 2", "depreciation_method": "Straight Line", "frequency_of_depreciation": 12, "total_number_of_depreciations": 6, @@ -1292,13 +1351,15 @@ class TestDepreciationBasics(AssetSetup): ) asset.save() - self.assertEqual(len(asset.schedules), 9) + asset_depr_schedule_doc_1 = get_asset_depr_schedule_doc( + asset.name, "Draft", "Test Finance Book 1" + ) + self.assertEqual(len(asset_depr_schedule_doc_1.get("depreciation_schedule")), 3) - for schedule in asset.schedules: - if schedule.idx <= 3: - self.assertEqual(schedule.finance_book_id, 1) - else: - self.assertEqual(schedule.finance_book_id, 2) + asset_depr_schedule_doc_2 = get_asset_depr_schedule_doc( + asset.name, "Draft", "Test Finance Book 2" + ) + self.assertEqual(len(asset_depr_schedule_doc_2.get("depreciation_schedule")), 6) def test_depreciation_entry_cancellation(self): asset = create_asset( @@ -1318,12 +1379,12 @@ class TestDepreciationBasics(AssetSetup): asset.load_from_db() # cancel depreciation entry - depr_entry = asset.get("schedules")[0].journal_entry + depr_entry = get_depr_schedule(asset.name, "Active")[0].journal_entry self.assertTrue(depr_entry) + frappe.get_doc("Journal Entry", depr_entry).cancel() - asset.load_from_db() - depr_entry = asset.get("schedules")[0].journal_entry + depr_entry = get_depr_schedule(asset.name, "Active")[0].journal_entry self.assertFalse(depr_entry) def test_asset_expected_value_after_useful_life(self): @@ -1338,7 +1399,7 @@ class TestDepreciationBasics(AssetSetup): ) accumulated_depreciation_after_full_schedule = max( - d.accumulated_depreciation_amount for d in asset.get("schedules") + d.accumulated_depreciation_amount for d in get_depr_schedule(asset.name, "Draft") ) asset_value_after_full_schedule = flt(asset.gross_purchase_amount) - flt( @@ -1369,7 +1430,7 @@ class TestDepreciationBasics(AssetSetup): asset.load_from_db() # check depreciation entry series - self.assertEqual(asset.get("schedules")[0].journal_entry[:4], "DEPR") + self.assertEqual(get_depr_schedule(asset.name, "Active")[0].journal_entry[:4], "DEPR") expected_gle = ( ("_Test Accumulated Depreciations - _TC", 0.0, 30000.0), @@ -1439,7 +1500,7 @@ class TestDepreciationBasics(AssetSetup): "2020-07-15", ] - for i, schedule in enumerate(asset.schedules): + for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")): self.assertEqual(getdate(expected_dates[i]), getdate(schedule.schedule_date)) @@ -1453,6 +1514,15 @@ def create_asset_data(): if not frappe.db.exists("Location", "Test Location"): frappe.get_doc({"doctype": "Location", "location_name": "Test Location"}).insert() + if not frappe.db.exists("Finance Book", "Test Finance Book 1"): + frappe.get_doc({"doctype": "Finance Book", "finance_book_name": "Test Finance Book 1"}).insert() + + if not frappe.db.exists("Finance Book", "Test Finance Book 2"): + frappe.get_doc({"doctype": "Finance Book", "finance_book_name": "Test Finance Book 2"}).insert() + + if not frappe.db.exists("Finance Book", "Test Finance Book 3"): + frappe.get_doc({"doctype": "Finance Book", "finance_book_name": "Test Finance Book 3"}).insert() + def create_asset(**args): args = frappe._dict(args) diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index 08355f047e5..7d3b645be7d 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -7,7 +7,7 @@ import frappe # import erpnext from frappe import _ -from frappe.utils import cint, flt +from frappe.utils import cint, flt, get_link_to_form from six import string_types import erpnext @@ -19,6 +19,9 @@ from erpnext.assets.doctype.asset.depreciation import ( reverse_depreciation_entry_made_after_disposal, ) from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account +from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( + make_new_active_asset_depr_schedules_and_cancel_current_ones, +) from erpnext.assets.doctype.asset_value_adjustment.asset_value_adjustment import ( get_current_asset_value, ) @@ -427,7 +430,12 @@ class AssetCapitalization(StockController): asset = self.get_asset(item) if asset.calculate_depreciation: - depreciate_asset(asset, self.posting_date) + notes = _( + "This schedule was created when Asset {0} was consumed when Asset Capitalization {1} was submitted." + ).format( + get_link_to_form(asset.doctype, asset.name), get_link_to_form(self.doctype, self.get("name")) + ) + depreciate_asset(asset, self.posting_date, notes) asset.reload() fixed_asset_gl_entries = get_gl_entries_on_asset_disposal( @@ -513,7 +521,12 @@ class AssetCapitalization(StockController): asset_doc.purchase_date = self.posting_date asset_doc.gross_purchase_amount = total_target_asset_value asset_doc.purchase_receipt_amount = total_target_asset_value - asset_doc.prepare_depreciation_data() + notes = _( + "This schedule was created when target Asset {0} was updated when Asset Capitalization {1} was submitted." + ).format( + get_link_to_form(asset_doc.doctype, asset_doc.name), get_link_to_form(self.doctype, self.name) + ) + make_new_active_asset_depr_schedules_and_cancel_current_ones(asset_doc, notes) asset_doc.flags.ignore_validate_update_after_submit = True asset_doc.save() elif self.docstatus == 2: @@ -524,7 +537,12 @@ class AssetCapitalization(StockController): if asset.calculate_depreciation: reverse_depreciation_entry_made_after_disposal(asset, self.posting_date) - reset_depreciation_schedule(asset, self.posting_date) + notes = _( + "This schedule was created when Asset {0} was restored when Asset Capitalization {1} was cancelled." + ).format( + get_link_to_form(asset.doctype, asset.name), get_link_to_form(self.doctype, self.name) + ) + reset_depreciation_schedule(asset, self.posting_date, notes) def get_asset(self, item): asset = frappe.get_doc("Asset", item.asset) diff --git a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py index 86861f0b165..4d519a60be7 100644 --- a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py @@ -12,6 +12,9 @@ from erpnext.assets.doctype.asset.test_asset import ( create_asset_data, set_depreciation_settings_in_company, ) +from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( + get_asset_depr_schedule_doc, +) from erpnext.stock.doctype.item.test_item import create_item @@ -253,6 +256,9 @@ class TestAssetCapitalization(unittest.TestCase): submit=1, ) + first_asset_depr_schedule = get_asset_depr_schedule_doc(consumed_asset.name, "Active") + self.assertEquals(first_asset_depr_schedule.status, "Active") + # Create and submit Asset Captitalization asset_capitalization = create_asset_capitalization( entry_type="Decapitalization", @@ -282,8 +288,18 @@ class TestAssetCapitalization(unittest.TestCase): consumed_asset.reload() self.assertEqual(consumed_asset.status, "Decapitalized") + first_asset_depr_schedule.load_from_db() + + second_asset_depr_schedule = get_asset_depr_schedule_doc(consumed_asset.name, "Active") + self.assertEquals(second_asset_depr_schedule.status, "Active") + self.assertEquals(first_asset_depr_schedule.status, "Cancelled") + + depr_schedule_of_consumed_asset = second_asset_depr_schedule.get("depreciation_schedule") + consumed_depreciation_schedule = [ - d for d in consumed_asset.schedules if getdate(d.schedule_date) == getdate(capitalization_date) + d + for d in depr_schedule_of_consumed_asset + if getdate(d.schedule_date) == getdate(capitalization_date) ] self.assertTrue( consumed_depreciation_schedule and consumed_depreciation_schedule[0].journal_entry diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/__init__.py b/erpnext/assets/doctype/asset_depreciation_schedule/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.js b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.js new file mode 100644 index 00000000000..c28b2b3b6a3 --- /dev/null +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.js @@ -0,0 +1,51 @@ +// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +frappe.provide("erpnext.asset"); + +frappe.ui.form.on('Asset Depreciation Schedule', { + onload: function(frm) { + frm.events.make_schedules_editable(frm); + }, + + make_schedules_editable: function(frm) { + var is_editable = frm.doc.depreciation_method == "Manual" ? true : false; + + frm.toggle_enable("depreciation_schedule", is_editable); + frm.fields_dict["depreciation_schedule"].grid.toggle_enable("schedule_date", is_editable); + frm.fields_dict["depreciation_schedule"].grid.toggle_enable("depreciation_amount", is_editable); + } +}); + +frappe.ui.form.on('Depreciation Schedule', { + make_depreciation_entry: function(frm, cdt, cdn) { + var row = locals[cdt][cdn]; + if (!row.journal_entry) { + frappe.call({ + method: "erpnext.assets.doctype.asset.depreciation.make_depreciation_entry", + args: { + "asset_depr_schedule_name": frm.doc.name, + "date": row.schedule_date + }, + callback: function(r) { + frappe.model.sync(r.message); + frm.refresh(); + } + }) + } + }, + + depreciation_amount: function(frm, cdt, cdn) { + erpnext.asset.set_accumulated_depreciation(frm); + } +}); + +erpnext.asset.set_accumulated_depreciation = function(frm) { + if(frm.doc.depreciation_method != "Manual") return; + + var accumulated_depreciation = flt(frm.doc.opening_accumulated_depreciation); + $.each(frm.doc.schedules || [], function(i, row) { + accumulated_depreciation += flt(row.depreciation_amount); + frappe.model.set_value(row.doctype, row.name, + "accumulated_depreciation_amount", accumulated_depreciation); + }) +}; diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json new file mode 100644 index 00000000000..af09cda8fb3 --- /dev/null +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json @@ -0,0 +1,202 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "naming_series:", + "creation": "2022-10-31 15:03:35.424877", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "asset", + "naming_series", + "column_break_2", + "opening_accumulated_depreciation", + "finance_book", + "finance_book_id", + "depreciation_details_section", + "depreciation_method", + "total_number_of_depreciations", + "rate_of_depreciation", + "column_break_8", + "frequency_of_depreciation", + "expected_value_after_useful_life", + "depreciation_schedule_section", + "depreciation_schedule", + "details_section", + "notes", + "status", + "amended_from" + ], + "fields": [ + { + "fieldname": "asset", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Asset", + "options": "Asset", + "reqd": 1 + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Naming Series", + "options": "ACC-ADS-.YYYY.-" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Asset Depreciation Schedule", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "collapsible": 1, + "fieldname": "depreciation_details_section", + "fieldtype": "Section Break", + "label": "Depreciation Details" + }, + { + "fieldname": "finance_book", + "fieldtype": "Link", + "label": "Finance Book", + "options": "Finance Book" + }, + { + "fieldname": "depreciation_method", + "fieldtype": "Select", + "label": "Depreciation Method", + "options": "\nStraight Line\nDouble Declining Balance\nWritten Down Value\nManual", + "read_only": 1 + }, + { + "depends_on": "eval:doc.depreciation_method == 'Written Down Value'", + "description": "In Percentage", + "fieldname": "rate_of_depreciation", + "fieldtype": "Percent", + "label": "Rate of Depreciation", + "read_only": 1 + }, + { + "fieldname": "column_break_8", + "fieldtype": "Column Break" + }, + { + "depends_on": "total_number_of_depreciations", + "fieldname": "total_number_of_depreciations", + "fieldtype": "Int", + "label": "Total Number of Depreciations", + "read_only": 1 + }, + { + "fieldname": "depreciation_schedule_section", + "fieldtype": "Section Break", + "label": "Depreciation Schedule" + }, + { + "fieldname": "depreciation_schedule", + "fieldtype": "Table", + "label": "Depreciation Schedule", + "options": "Depreciation Schedule" + }, + { + "collapsible": 1, + "collapsible_depends_on": "notes", + "fieldname": "details_section", + "fieldtype": "Section Break", + "label": "Details" + }, + { + "fieldname": "notes", + "fieldtype": "Small Text", + "label": "Notes", + "read_only": 1 + }, + { + "fieldname": "status", + "fieldtype": "Select", + "hidden": 1, + "label": "Status", + "options": "Draft\nActive\nCancelled", + "read_only": 1 + }, + { + "depends_on": "frequency_of_depreciation", + "fieldname": "frequency_of_depreciation", + "fieldtype": "Int", + "label": "Frequency of Depreciation (Months)", + "read_only": 1 + }, + { + "fieldname": "expected_value_after_useful_life", + "fieldtype": "Currency", + "label": "Expected Value After Useful Life", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "finance_book_id", + "fieldtype": "Int", + "hidden": 1, + "label": "Finance Book Id", + "print_hide": 1, + "read_only": 1 + }, + { + "depends_on": "opening_accumulated_depreciation", + "fieldname": "opening_accumulated_depreciation", + "fieldtype": "Currency", + "label": "Opening Accumulated Depreciation", + "options": "Company:company:default_currency", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2023-01-02 15:38:30.766779", + "modified_by": "Administrator", + "module": "Assets", + "name": "Asset Depreciation Schedule", + "naming_rule": "By \"Naming Series\" field", + "owner": "Administrator", + "permissions": [ + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Quality Manager", + "share": 1, + "submit": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py new file mode 100644 index 00000000000..1446a6e7a2d --- /dev/null +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py @@ -0,0 +1,516 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.model.document import Document +from frappe.utils import ( + add_days, + add_months, + cint, + date_diff, + flt, + get_last_day, + is_last_day_of_the_month, +) + +import erpnext + + +class AssetDepreciationSchedule(Document): + def before_save(self): + if not self.finance_book_id: + self.prepare_draft_asset_depr_schedule_data_from_asset_name_and_fb_name( + self.asset, self.finance_book + ) + + def validate(self): + self.validate_another_asset_depr_schedule_does_not_exist() + + def validate_another_asset_depr_schedule_does_not_exist(self): + finance_book_filter = ["finance_book", "is", "not set"] + if self.finance_book: + finance_book_filter = ["finance_book", "=", self.finance_book] + + asset_depr_schedule = frappe.db.exists( + "Asset Depreciation Schedule", + [ + ["asset", "=", self.asset], + finance_book_filter, + ["docstatus", "<", 2], + ], + ) + + if asset_depr_schedule and asset_depr_schedule != self.name: + if self.finance_book: + frappe.throw( + _( + "Asset Depreciation Schedule {0} for Asset {1} and Finance Book {2} already exists." + ).format(asset_depr_schedule, self.asset, self.finance_book) + ) + else: + frappe.throw( + _("Asset Depreciation Schedule {0} for Asset {1} already exists.").format( + asset_depr_schedule, self.asset + ) + ) + + def on_submit(self): + self.db_set("status", "Active") + + def before_cancel(self): + if not self.flags.should_not_cancel_depreciation_entries: + self.cancel_depreciation_entries() + + def cancel_depreciation_entries(self): + for d in self.get("depreciation_schedule"): + if d.journal_entry: + frappe.get_doc("Journal Entry", d.journal_entry).cancel() + + def on_cancel(self): + self.db_set("status", "Cancelled") + + def prepare_draft_asset_depr_schedule_data_from_asset_name_and_fb_name(self, asset_name, fb_name): + asset_doc = frappe.get_doc("Asset", asset_name) + + finance_book_filter = ["finance_book", "is", "not set"] + if fb_name: + finance_book_filter = ["finance_book", "=", fb_name] + + asset_finance_book_name = frappe.db.get_value( + doctype="Asset Finance Book", + filters=[["parent", "=", asset_name], finance_book_filter], + ) + asset_finance_book_doc = frappe.get_doc("Asset Finance Book", asset_finance_book_name) + + prepare_draft_asset_depr_schedule_data(self, asset_doc, asset_finance_book_doc) + + +def make_draft_asset_depr_schedules_if_not_present(asset_doc): + for row in asset_doc.get("finance_books"): + draft_asset_depr_schedule_name = get_asset_depr_schedule_name( + asset_doc.name, "Draft", row.finance_book + ) + + active_asset_depr_schedule_name = get_asset_depr_schedule_name( + asset_doc.name, "Active", row.finance_book + ) + + if not draft_asset_depr_schedule_name and not active_asset_depr_schedule_name: + make_draft_asset_depr_schedule(asset_doc, row) + + +def make_draft_asset_depr_schedules(asset_doc): + for row in asset_doc.get("finance_books"): + make_draft_asset_depr_schedule(asset_doc, row) + + +def make_draft_asset_depr_schedule(asset_doc, row): + asset_depr_schedule_doc = frappe.new_doc("Asset Depreciation Schedule") + + prepare_draft_asset_depr_schedule_data(asset_depr_schedule_doc, asset_doc, row) + + asset_depr_schedule_doc.insert() + + +def update_draft_asset_depr_schedules(asset_doc): + for row in asset_doc.get("finance_books"): + asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset_doc.name, "Draft", row.finance_book) + + if not asset_depr_schedule_doc: + continue + + prepare_draft_asset_depr_schedule_data(asset_depr_schedule_doc, asset_doc, row) + + asset_depr_schedule_doc.save() + + +def prepare_draft_asset_depr_schedule_data( + asset_depr_schedule_doc, + asset_doc, + row, + date_of_disposal=None, + date_of_return=None, + update_asset_finance_book_row=True, +): + set_draft_asset_depr_schedule_details(asset_depr_schedule_doc, asset_doc, row) + make_depr_schedule( + asset_depr_schedule_doc, asset_doc, row, date_of_disposal, update_asset_finance_book_row + ) + set_accumulated_depreciation(asset_depr_schedule_doc, row, date_of_disposal, date_of_return) + + +def set_draft_asset_depr_schedule_details(asset_depr_schedule_doc, asset_doc, row): + asset_depr_schedule_doc.asset = asset_doc.name + asset_depr_schedule_doc.finance_book = row.finance_book + asset_depr_schedule_doc.finance_book_id = row.idx + asset_depr_schedule_doc.opening_accumulated_depreciation = ( + asset_doc.opening_accumulated_depreciation + ) + asset_depr_schedule_doc.depreciation_method = row.depreciation_method + asset_depr_schedule_doc.total_number_of_depreciations = row.total_number_of_depreciations + asset_depr_schedule_doc.frequency_of_depreciation = row.frequency_of_depreciation + asset_depr_schedule_doc.rate_of_depreciation = row.rate_of_depreciation + asset_depr_schedule_doc.expected_value_after_useful_life = row.expected_value_after_useful_life + asset_depr_schedule_doc.status = "Draft" + + +def convert_draft_asset_depr_schedules_into_active(asset_doc): + for row in asset_doc.get("finance_books"): + asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset_doc.name, "Draft", row.finance_book) + + if not asset_depr_schedule_doc: + continue + + asset_depr_schedule_doc.submit() + + +def cancel_asset_depr_schedules(asset_doc): + for row in asset_doc.get("finance_books"): + asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset_doc.name, "Active", row.finance_book) + + if not asset_depr_schedule_doc: + continue + + asset_depr_schedule_doc.cancel() + + +def make_new_active_asset_depr_schedules_and_cancel_current_ones( + asset_doc, notes, date_of_disposal=None, date_of_return=None +): + for row in asset_doc.get("finance_books"): + current_asset_depr_schedule_doc = get_asset_depr_schedule_doc( + asset_doc.name, "Active", row.finance_book + ) + + if not current_asset_depr_schedule_doc: + frappe.throw( + _("Asset Depreciation Schedule not found for Asset {0} and Finance Book {1}").format( + asset_doc.name, row.finance_book + ) + ) + + new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc) + + make_depr_schedule(new_asset_depr_schedule_doc, asset_doc, row, date_of_disposal) + set_accumulated_depreciation(new_asset_depr_schedule_doc, row, date_of_disposal, date_of_return) + + new_asset_depr_schedule_doc.notes = notes + + current_asset_depr_schedule_doc.flags.should_not_cancel_depreciation_entries = True + current_asset_depr_schedule_doc.cancel() + + new_asset_depr_schedule_doc.submit() + + +def get_temp_asset_depr_schedule_doc( + asset_doc, row, date_of_disposal=None, date_of_return=None, update_asset_finance_book_row=False +): + asset_depr_schedule_doc = frappe.new_doc("Asset Depreciation Schedule") + + prepare_draft_asset_depr_schedule_data( + asset_depr_schedule_doc, + asset_doc, + row, + date_of_disposal, + date_of_return, + update_asset_finance_book_row, + ) + + return asset_depr_schedule_doc + + +def get_asset_depr_schedule_name(asset_name, status, finance_book=None): + finance_book_filter = ["finance_book", "is", "not set"] + if finance_book: + finance_book_filter = ["finance_book", "=", finance_book] + + return frappe.db.get_value( + doctype="Asset Depreciation Schedule", + filters=[ + ["asset", "=", asset_name], + finance_book_filter, + ["status", "=", status], + ], + ) + + +@frappe.whitelist() +def get_depr_schedule(asset_name, status, finance_book=None): + asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset_name, status, finance_book) + + if not asset_depr_schedule_doc: + return + + return asset_depr_schedule_doc.get("depreciation_schedule") + + +def get_asset_depr_schedule_doc(asset_name, status, finance_book=None): + asset_depr_schedule_name = get_asset_depr_schedule_name(asset_name, status, finance_book) + + if not asset_depr_schedule_name: + return + + asset_depr_schedule_doc = frappe.get_doc("Asset Depreciation Schedule", asset_depr_schedule_name) + + return asset_depr_schedule_doc + + +def make_depr_schedule( + asset_depr_schedule_doc, asset_doc, row, date_of_disposal, update_asset_finance_book_row=True +): + if row.depreciation_method != "Manual" and not asset_depr_schedule_doc.get( + "depreciation_schedule" + ): + asset_depr_schedule_doc.depreciation_schedule = [] + + if not asset_doc.available_for_use_date: + return + + start = clear_depr_schedule(asset_depr_schedule_doc) + + _make_depr_schedule( + asset_depr_schedule_doc, asset_doc, row, start, date_of_disposal, update_asset_finance_book_row + ) + + +def clear_depr_schedule(asset_depr_schedule_doc): + start = 0 + num_of_depreciations_completed = 0 + depr_schedule = [] + + for schedule in asset_depr_schedule_doc.get("depreciation_schedule"): + if schedule.journal_entry: + num_of_depreciations_completed += 1 + depr_schedule.append(schedule) + else: + start = num_of_depreciations_completed + break + + asset_depr_schedule_doc.depreciation_schedule = depr_schedule + + return start + + +def _make_depr_schedule( + asset_depr_schedule_doc, asset_doc, row, start, date_of_disposal, update_asset_finance_book_row +): + asset_doc.validate_asset_finance_books(row) + + value_after_depreciation = asset_doc._get_value_after_depreciation(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( + asset_doc.number_of_depreciations_booked + ) + + has_pro_rata = asset_doc.check_is_pro_rata(row) + if has_pro_rata: + number_of_pending_depreciations += 1 + + skip_row = False + should_get_last_day = is_last_day_of_the_month(row.depreciation_start_date) + + for n in range(start, number_of_pending_depreciations): + # If depreciation is already completed (for double declining balance) + if skip_row: + continue + + depreciation_amount = get_depreciation_amount(asset_doc, value_after_depreciation, row) + + if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1: + schedule_date = add_months(row.depreciation_start_date, n * cint(row.frequency_of_depreciation)) + + if should_get_last_day: + schedule_date = get_last_day(schedule_date) + + # schedule date will be a year later from start date + # so monthly schedule date is calculated by removing 11 months from it + monthly_schedule_date = add_months(schedule_date, -row.frequency_of_depreciation + 1) + + # if asset is being sold or scrapped + if date_of_disposal: + from_date = asset_doc.available_for_use_date + if asset_depr_schedule_doc.depreciation_schedule: + from_date = asset_depr_schedule_doc.depreciation_schedule[-1].schedule_date + + depreciation_amount, days, months = asset_doc.get_pro_rata_amt( + row, depreciation_amount, from_date, date_of_disposal + ) + + if depreciation_amount > 0: + add_depr_schedule_row( + asset_depr_schedule_doc, + date_of_disposal, + depreciation_amount, + row.depreciation_method, + ) + + break + + # For first row + if has_pro_rata and not asset_doc.opening_accumulated_depreciation and n == 0: + from_date = add_days( + asset_doc.available_for_use_date, -1 + ) # needed to calc depr amount for available_for_use_date too + depreciation_amount, days, months = asset_doc.get_pro_rata_amt( + row, depreciation_amount, from_date, row.depreciation_start_date + ) + + # For first depr schedule date will be the start date + # so monthly schedule date is calculated by removing + # month difference between use date and start date + monthly_schedule_date = add_months(row.depreciation_start_date, -months + 1) + + # For last row + elif has_pro_rata and n == cint(number_of_pending_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( + asset_doc.available_for_use_date, + (n + asset_doc.number_of_depreciations_booked) * cint(row.frequency_of_depreciation), + ) + + depreciation_amount_without_pro_rata = depreciation_amount + + depreciation_amount, days, months = asset_doc.get_pro_rata_amt( + row, depreciation_amount, schedule_date, asset_doc.to_date + ) + + depreciation_amount = get_adjusted_depreciation_amount( + asset_depr_schedule_doc, depreciation_amount_without_pro_rata, depreciation_amount + ) + + monthly_schedule_date = add_months(schedule_date, 1) + schedule_date = add_days(schedule_date, days) + last_schedule_date = schedule_date + + if not depreciation_amount: + continue + value_after_depreciation -= flt( + depreciation_amount, asset_doc.precision("gross_purchase_amount") + ) + + # 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 + and value_after_depreciation != row.expected_value_after_useful_life + ) + or value_after_depreciation < row.expected_value_after_useful_life + ): + depreciation_amount += value_after_depreciation - row.expected_value_after_useful_life + skip_row = True + + if depreciation_amount > 0: + add_depr_schedule_row( + asset_depr_schedule_doc, + schedule_date, + depreciation_amount, + row.depreciation_method, + ) + + +# to ensure that final accumulated depreciation amount is accurate +def get_adjusted_depreciation_amount( + asset_depr_schedule_doc, depreciation_amount_without_pro_rata, depreciation_amount_for_last_row +): + if not asset_depr_schedule_doc.opening_accumulated_depreciation: + depreciation_amount_for_first_row = get_depreciation_amount_for_first_row( + asset_depr_schedule_doc + ) + + if ( + depreciation_amount_for_first_row + depreciation_amount_for_last_row + != depreciation_amount_without_pro_rata + ): + depreciation_amount_for_last_row = ( + depreciation_amount_without_pro_rata - depreciation_amount_for_first_row + ) + + return depreciation_amount_for_last_row + + +def get_depreciation_amount_for_first_row(asset_depr_schedule_doc): + return asset_depr_schedule_doc.get("depreciation_schedule")[0].depreciation_amount + + +@erpnext.allow_regional +def get_depreciation_amount(asset_doc, depreciable_value, row): + if row.depreciation_method in ("Straight Line", "Manual"): + # if the Depreciation Schedule is being prepared for the first time + if not asset_doc.flags.increase_in_asset_life: + depreciation_amount = ( + flt(asset_doc.gross_purchase_amount) - flt(row.expected_value_after_useful_life) + ) / flt(row.total_number_of_depreciations) + + # if the Depreciation Schedule is being modified after Asset Repair + else: + depreciation_amount = ( + flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life) + ) / (date_diff(asset_doc.to_date, asset_doc.available_for_use_date) / 365) + else: + depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100)) + + return depreciation_amount + + +def add_depr_schedule_row( + asset_depr_schedule_doc, + schedule_date, + depreciation_amount, + depreciation_method, +): + asset_depr_schedule_doc.append( + "depreciation_schedule", + { + "schedule_date": schedule_date, + "depreciation_amount": depreciation_amount, + "depreciation_method": depreciation_method, + }, + ) + + +def set_accumulated_depreciation( + asset_depr_schedule_doc, + row, + date_of_disposal=None, + date_of_return=None, + ignore_booked_entry=False, +): + straight_line_idx = [ + d.idx + for d in asset_depr_schedule_doc.get("depreciation_schedule") + if d.depreciation_method == "Straight Line" + ] + + accumulated_depreciation = flt(asset_depr_schedule_doc.opening_accumulated_depreciation) + value_after_depreciation = flt(row.value_after_depreciation) + + for i, d in enumerate(asset_depr_schedule_doc.get("depreciation_schedule")): + if ignore_booked_entry and d.journal_entry: + continue + + depreciation_amount = flt(d.depreciation_amount, d.precision("depreciation_amount")) + value_after_depreciation -= flt(depreciation_amount) + + # for the last row, if depreciation method = Straight Line + if ( + straight_line_idx + and i == max(straight_line_idx) - 1 + and not date_of_disposal + and not date_of_return + ): + depreciation_amount += flt( + value_after_depreciation - flt(row.expected_value_after_useful_life), + d.precision("depreciation_amount"), + ) + + d.depreciation_amount = depreciation_amount + accumulated_depreciation += d.depreciation_amount + d.accumulated_depreciation_amount = flt( + accumulated_depreciation, d.precision("accumulated_depreciation_amount") + ) diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/test_asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/test_asset_depreciation_schedule.py new file mode 100644 index 00000000000..024121d3943 --- /dev/null +++ b/erpnext/assets/doctype/asset_depreciation_schedule/test_asset_depreciation_schedule.py @@ -0,0 +1,27 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.tests.utils import FrappeTestCase + +from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data +from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( + get_asset_depr_schedule_doc, +) + + +class TestAssetDepreciationSchedule(FrappeTestCase): + def setUp(self): + create_asset_data() + + def test_throw_error_if_another_asset_depr_schedule_exist(self): + asset = create_asset(item_code="Macbook Pro", calculate_depreciation=1, submit=1) + + first_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active") + self.assertEquals(first_asset_depr_schedule.status, "Active") + + second_asset_depr_schedule = frappe.get_doc( + {"doctype": "Asset Depreciation Schedule", "asset": asset.name, "finance_book": None} + ) + + self.assertRaises(frappe.ValidationError, second_asset_depr_schedule.insert) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index d5913c59463..b8cd115872c 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -3,11 +3,15 @@ import frappe from frappe import _ -from frappe.utils import add_months, cint, flt, getdate, time_diff_in_hours +from frappe.utils import add_months, cint, flt, get_link_to_form, getdate, time_diff_in_hours import erpnext from erpnext.accounts.general_ledger import make_gl_entries from erpnext.assets.doctype.asset.asset import get_asset_account +from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( + get_depr_schedule, + make_new_active_asset_depr_schedules_and_cancel_current_ones, +) from erpnext.controllers.accounts_controller import AccountsController @@ -52,8 +56,11 @@ class AssetRepair(AccountsController): ): self.modify_depreciation_schedule() + notes = _("This schedule was created when Asset Repair {0} was submitted.").format( + get_link_to_form(self.doctype, self.name) + ) self.asset_doc.flags.ignore_validate_update_after_submit = True - self.asset_doc.prepare_depreciation_data() + make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes) self.asset_doc.save() def before_cancel(self): @@ -73,8 +80,11 @@ class AssetRepair(AccountsController): ): self.revert_depreciation_schedule_on_cancellation() + notes = _("This schedule was created when Asset Repair {0} was cancelled.").format( + get_link_to_form(self.doctype, self.name) + ) self.asset_doc.flags.ignore_validate_update_after_submit = True - self.asset_doc.prepare_depreciation_data() + make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes) self.asset_doc.save() def check_repair_status(self): @@ -279,8 +289,10 @@ class AssetRepair(AccountsController): asset.number_of_depreciations_booked ) + depr_schedule = get_depr_schedule(asset.name, "Active", row.finance_book) + # the Schedule Date in the final row of the old Depreciation Schedule - last_schedule_date = asset.schedules[len(asset.schedules) - 1].schedule_date + last_schedule_date = depr_schedule[len(depr_schedule) - 1].schedule_date # the Schedule Date in the final row of the new Depreciation Schedule asset.to_date = add_months(last_schedule_date, extra_months) @@ -310,8 +322,10 @@ class AssetRepair(AccountsController): asset.number_of_depreciations_booked ) + depr_schedule = get_depr_schedule(asset.name, "Active", row.finance_book) + # the Schedule Date in the final row of the modified Depreciation Schedule - last_schedule_date = asset.schedules[len(asset.schedules) - 1].schedule_date + last_schedule_date = depr_schedule[len(depr_schedule) - 1].schedule_date # the Schedule Date in the final row of the original Depreciation Schedule asset.to_date = add_months(last_schedule_date, -extra_months) diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py index 6e06f52ac65..ff72aa94b99 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -12,6 +12,9 @@ from erpnext.assets.doctype.asset.test_asset import ( create_asset_data, set_depreciation_settings_in_company, ) +from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( + get_asset_depr_schedule_doc, +) from erpnext.stock.doctype.item.test_item import create_item @@ -232,13 +235,23 @@ class TestAssetRepair(unittest.TestCase): def test_increase_in_asset_life(self): asset = create_asset(calculate_depreciation=1, submit=1) + + first_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active") + self.assertEquals(first_asset_depr_schedule.status, "Active") + initial_num_of_depreciations = num_of_depreciations(asset) create_asset_repair(asset=asset, capitalize_repair_cost=1, submit=1) + asset.reload() + first_asset_depr_schedule.load_from_db() + + second_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active") + self.assertEquals(second_asset_depr_schedule.status, "Active") + self.assertEquals(first_asset_depr_schedule.status, "Cancelled") self.assertEqual((initial_num_of_depreciations + 1), num_of_depreciations(asset)) self.assertEqual( - asset.schedules[-1].accumulated_depreciation_amount, + second_asset_depr_schedule.get("depreciation_schedule")[-1].accumulated_depreciation_amount, asset.finance_books[0].value_after_depreciation, ) 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 84aa8fa0239..262d5529974 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py @@ -5,13 +5,17 @@ import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import cint, date_diff, flt, formatdate, getdate +from frappe.utils import date_diff, flt, formatdate, get_link_to_form, getdate from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_checks_for_pl_and_bs_accounts, ) -from erpnext.assets.doctype.asset.asset import get_depreciation_amount from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts +from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( + get_asset_depr_schedule_doc, + get_depreciation_amount, + set_accumulated_depreciation, +) class AssetValueAdjustment(Document): @@ -112,21 +116,40 @@ class AssetValueAdjustment(Document): for d in asset.finance_books: d.value_after_depreciation = asset_value + current_asset_depr_schedule_doc = get_asset_depr_schedule_doc( + asset.name, "Active", d.finance_book + ) + + 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() + + 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")), + ) + 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 asset.schedules if cint(s.finance_book_id) == d.idx) + 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) / flt(total_days) from_date = self.date else: - no_of_depreciations = len( - [ - s.name for s in asset.schedules if (cint(s.finance_book_id) == d.idx and not s.journal_entry) - ] - ) + 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 asset.schedules: - if cint(data.finance_book_id) == d.idx and not data.journal_entry: + 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 @@ -140,10 +163,12 @@ class AssetValueAdjustment(Document): d.db_update() - asset.set_accumulated_depreciation(ignore_booked_entry=True) - for asset_data in asset.schedules: - if not asset_data.journal_entry: - asset_data.db_update() + set_accumulated_depreciation(new_asset_depr_schedule_doc, 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() @frappe.whitelist() 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 62c636624ce..03dcea96c53 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 @@ -7,6 +7,9 @@ import frappe from frappe.utils import add_days, get_last_day, nowdate 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, +) from erpnext.assets.doctype.asset_value_adjustment.asset_value_adjustment import ( get_current_asset_value, ) @@ -73,12 +76,21 @@ class TestAssetValueAdjustment(unittest.TestCase): ) asset_doc.submit() + first_asset_depr_schedule = get_asset_depr_schedule_doc(asset_doc.name, "Active") + self.assertEquals(first_asset_depr_schedule.status, "Active") + current_value = get_current_asset_value(asset_doc.name) adj_doc = make_asset_value_adjustment( asset=asset_doc.name, current_asset_value=current_value, new_asset_value=50000.0 ) adj_doc.submit() + first_asset_depr_schedule.load_from_db() + + second_asset_depr_schedule = get_asset_depr_schedule_doc(asset_doc.name, "Active") + self.assertEquals(second_asset_depr_schedule.status, "Active") + 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), diff --git a/erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json b/erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json index 35a2c9dd7f3..882c4bf00b1 100644 --- a/erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json +++ b/erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json @@ -1,318 +1,84 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 1, - "autoname": "", - "beta": 0, - "creation": "2016-03-02 15:11:01.278862", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "allow_rename": 1, + "creation": "2016-03-02 15:11:01.278862", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "schedule_date", + "depreciation_amount", + "column_break_3", + "accumulated_depreciation_amount", + "journal_entry", + "make_depreciation_entry", + "depreciation_method" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "finance_book", - "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": "Finance Book", - "length": 0, - "no_copy": 0, - "options": "Finance Book", - "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, - "unique": 0 - }, + "fieldname": "schedule_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Schedule Date", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "schedule_date", - "fieldtype": "Date", - "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": "Schedule Date", - "length": 0, - "no_copy": 1, - "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, - "unique": 0 - }, + "fieldname": "depreciation_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Depreciation Amount", + "options": "Company:company:default_currency", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "depreciation_amount", - "fieldtype": "Currency", - "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": "Depreciation Amount", - "length": 0, - "no_copy": 1, - "options": "Company:company:default_currency", - "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, - "unique": 0 - }, + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_3", - "fieldtype": "Column Break", - "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, - "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, - "unique": 0 - }, + "fieldname": "accumulated_depreciation_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Accumulated Depreciation Amount", + "options": "Company:company:default_currency", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "accumulated_depreciation_amount", - "fieldtype": "Currency", - "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": "Accumulated Depreciation Amount", - "length": 0, - "no_copy": 1, - "options": "Company:company:default_currency", - "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, - "unique": 0 - }, + "depends_on": "eval:doc.docstatus==1", + "fieldname": "journal_entry", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Journal Entry", + "options": "Journal Entry", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.docstatus==1", - "fieldname": "journal_entry", - "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": "Journal Entry", - "length": 0, - "no_copy": 1, - "options": "Journal Entry", - "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, - "unique": 0 - }, + "allow_on_submit": 1, + "depends_on": "eval:(doc.docstatus==1 && !doc.journal_entry && doc.schedule_date <= get_today())", + "fieldname": "make_depreciation_entry", + "fieldtype": "Button", + "label": "Make Depreciation Entry" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:(doc.docstatus==1 && !doc.journal_entry && doc.schedule_date <= get_today())", - "fieldname": "make_depreciation_entry", - "fieldtype": "Button", - "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": "Make Depreciation Entry", - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "finance_book_id", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Finance Book Id", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "depreciation_method", - "fieldtype": "Select", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Depreciation Method", - "length": 0, - "no_copy": 1, - "options": "\nStraight Line\nDouble Declining Balance\nWritten Down Value\nManual", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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, - "unique": 0 + "fieldname": "depreciation_method", + "fieldtype": "Select", + "hidden": 1, + "label": "Depreciation Method", + "options": "\nStraight Line\nDouble Declining Balance\nWritten Down Value\nManual", + "print_hide": 1, + "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-05-10 15:12:41.679436", - "modified_by": "Administrator", - "module": "Assets", - "name": "Depreciation Schedule", - "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": 0, - "track_seen": 0 + ], + "istable": 1, + "links": [], + "modified": "2022-12-06 20:35:50.264281", + "modified_by": "Administrator", + "module": "Assets", + "name": "Depreciation Schedule", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py index 6b14dce084e..bb50df0ba28 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py @@ -176,15 +176,17 @@ def get_finance_book_value_map(filters): return frappe._dict( frappe.db.sql( """ Select - parent, SUM(depreciation_amount) - FROM `tabDepreciation Schedule` + ads.asset, SUM(depreciation_amount) + FROM `tabAsset Depreciation Schedule` ads, `tabDepreciation Schedule` ds WHERE - parentfield='schedules' - AND schedule_date<=%s - AND journal_entry IS NOT NULL - AND ifnull(finance_book, '')=%s - GROUP BY parent""", - (date, cstr(filters.finance_book or "")), + ds.parent = ads.name + AND ifnull(ads.finance_book, '')=%s + AND ads.docstatus=1 + AND ds.parentfield='depreciation_schedule' + AND ds.schedule_date<=%s + AND ds.journal_entry IS NOT NULL + GROUP BY ads.asset""", + (cstr(filters.finance_book or ""), date), ) ) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 2420a23bb03..74f866e20eb 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -268,6 +268,7 @@ erpnext.patches.v13_0.show_hr_payroll_deprecation_warning erpnext.patches.v13_0.reset_corrupt_defaults erpnext.patches.v13_0.create_accounting_dimensions_for_asset_repair erpnext.patches.v15_0.delete_taxjar_doctypes +erpnext.patches.v15_0.create_asset_depreciation_schedules_from_assets [post_model_sync] execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings') diff --git a/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py b/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py new file mode 100644 index 00000000000..5dc3cdde6f8 --- /dev/null +++ b/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py @@ -0,0 +1,80 @@ +import frappe + +from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( + set_draft_asset_depr_schedule_details, +) + + +def execute(): + frappe.reload_doc("assets", "doctype", "Asset Depreciation Schedule") + + assets = get_details_of_draft_or_submitted_depreciable_assets() + + for asset in assets: + finance_book_rows = get_details_of_asset_finance_books_rows(asset.name) + + for fb_row in finance_book_rows: + asset_depr_schedule_doc = frappe.new_doc("Asset Depreciation Schedule") + + set_draft_asset_depr_schedule_details(asset_depr_schedule_doc, asset, fb_row) + + asset_depr_schedule_doc.insert() + + if asset.docstatus == 1: + asset_depr_schedule_doc.submit() + + update_depreciation_schedules(asset.name, asset_depr_schedule_doc.name, fb_row.idx) + + +def get_details_of_draft_or_submitted_depreciable_assets(): + asset = frappe.qb.DocType("Asset") + + records = ( + frappe.qb.from_(asset) + .select(asset.name, asset.opening_accumulated_depreciation, asset.docstatus) + .where(asset.calculate_depreciation == 1) + .where(asset.docstatus < 2) + ).run(as_dict=True) + + return records + + +def get_details_of_asset_finance_books_rows(asset_name): + afb = frappe.qb.DocType("Asset Finance Book") + + records = ( + frappe.qb.from_(afb) + .select( + afb.finance_book, + afb.idx, + afb.depreciation_method, + afb.total_number_of_depreciations, + afb.frequency_of_depreciation, + afb.rate_of_depreciation, + afb.expected_value_after_useful_life, + ) + .where(afb.parent == asset_name) + ).run(as_dict=True) + + return records + + +def update_depreciation_schedules(asset_name, asset_depr_schedule_name, fb_row_idx): + ds = frappe.qb.DocType("Depreciation Schedule") + + depr_schedules = ( + frappe.qb.from_(ds) + .select(ds.name) + .where((ds.parent == asset_name) & (ds.finance_book_id == str(fb_row_idx))) + .orderby(ds.idx) + ).run(as_dict=True) + + for idx, depr_schedule in enumerate(depr_schedules, start=1): + ( + frappe.qb.update(ds) + .set(ds.idx, idx) + .set(ds.parent, asset_depr_schedule_name) + .set(ds.parentfield, "depreciation_schedule") + .set(ds.parenttype, "Asset Depreciation Schedule") + .where(ds.name == depr_schedule.name) + ).run()