diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json index 39c914c0c6a..c7430dbf00c 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.json +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json @@ -89,7 +89,7 @@ "label": "Entry Type", "oldfieldname": "voucher_type", "oldfieldtype": "Select", - "options": "Journal Entry\nInter Company Journal Entry\nBank Entry\nCash Entry\nCredit Card Entry\nDebit Note\nCredit Note\nContra Entry\nExcise Entry\nWrite Off Entry\nOpening Entry\nDepreciation Entry\nExchange Rate Revaluation\nExchange Gain Or Loss\nDeferred Revenue\nDeferred Expense", + "options": "Journal Entry\nInter Company Journal Entry\nBank Entry\nCash Entry\nCredit Card Entry\nDebit Note\nCredit Note\nContra Entry\nExcise Entry\nWrite Off Entry\nOpening Entry\nDepreciation Entry\nAsset Disposal\nExchange Rate Revaluation\nExchange Gain Or Loss\nDeferred Revenue\nDeferred Expense", "reqd": 1, "search_index": 1 }, @@ -557,7 +557,7 @@ "table_fieldname": "payment_entries" } ], - "modified": "2024-07-18 15:32:29.413598", + "modified": "2024-12-26 15:32:20.730666", "modified_by": "Administrator", "module": "Accounts", "name": "Journal Entry", diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index ea7583e6218..4a79b6ceb2c 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -100,6 +100,7 @@ class JournalEntry(AccountsController): "Write Off Entry", "Opening Entry", "Depreciation Entry", + "Asset Disposal", "Exchange Rate Revaluation", "Exchange Gain Or Loss", "Deferred Revenue", @@ -377,7 +378,11 @@ class JournalEntry(AccountsController): self.remove(d) def update_asset_value(self): - if self.flags.planned_depr_entry or self.voucher_type != "Depreciation Entry": + self.update_asset_on_depreciation() + self.update_asset_on_disposal() + + def update_asset_on_depreciation(self): + if self.voucher_type != "Depreciation Entry": return for d in self.get("accounts"): @@ -387,24 +392,61 @@ class JournalEntry(AccountsController): and d.account_type == "Depreciation" and d.debit ): - asset = frappe.get_doc("Asset", d.reference_name) + asset = frappe.get_cached_doc("Asset", d.reference_name) if asset.calculate_depreciation: - fb_idx = 1 - if self.finance_book: - for fb_row in asset.get("finance_books"): - if fb_row.finance_book == self.finance_book: - fb_idx = fb_row.idx - break - fb_row = asset.get("finance_books")[fb_idx - 1] - fb_row.value_after_depreciation -= d.debit - fb_row.db_update() + self.update_journal_entry_link_on_depr_schedule(asset, d) + self.update_value_after_depreciation(asset, d.debit) else: asset.db_set("value_after_depreciation", asset.value_after_depreciation - d.debit) asset.set_status() asset.set_total_booked_depreciations() + def update_value_after_depreciation(self, asset, depr_amount): + fb_idx = 1 + if self.finance_book: + for fb_row in asset.get("finance_books"): + if fb_row.finance_book == self.finance_book: + fb_idx = fb_row.idx + break + fb_row = asset.get("finance_books")[fb_idx - 1] + fb_row.value_after_depreciation -= depr_amount + frappe.db.set_value( + "Asset Finance Book", fb_row.name, "value_after_depreciation", fb_row.value_after_depreciation + ) + + def update_journal_entry_link_on_depr_schedule(self, asset, je_row): + depr_schedule = get_depr_schedule(asset.name, "Active", self.finance_book) + for d in depr_schedule or []: + if ( + d.schedule_date == self.posting_date + and not d.journal_entry + and d.depreciation_amount == flt(je_row.debit) + ): + frappe.db.set_value("Depreciation Schedule", d.name, "journal_entry", self.name) + + def update_asset_on_disposal(self): + if self.voucher_type == "Asset Disposal": + disposed_assets = [] + for d in self.get("accounts"): + if ( + d.reference_type == "Asset" + and d.reference_name + and d.reference_name not in disposed_assets + ): + frappe.db.set_value( + "Asset", + d.reference_name, + { + "disposal_date": self.posting_date, + "journal_entry_for_scrap": self.name, + }, + ) + asset_doc = frappe.get_doc("Asset", d.reference_name) + asset_doc.set_status() + disposed_assets.append(d.reference_name) + def update_inter_company_jv(self): if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference: frappe.db.set_value( diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 8eacada5e4a..62bb9c65dc2 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -39,7 +39,7 @@ from erpnext.assets.doctype.asset.depreciation import ( get_gl_entries_on_asset_disposal, get_gl_entries_on_asset_regain, reset_depreciation_schedule, - reverse_depreciation_entry_made_after_disposal, + reverse_depreciation_entry_made_on_disposal, ) from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity from erpnext.controllers.accounts_controller import validate_account_head @@ -368,21 +368,34 @@ class SalesInvoice(SellingController): validate_docs_for_deferred_accounting([self.name], []) def validate_fixed_asset(self): - for d in self.get("items"): - if d.is_fixed_asset and d.meta.get_field("asset") and d.asset: - asset = frappe.get_doc("Asset", d.asset) - if self.doctype == "Sales Invoice" and self.docstatus == 1: - if self.update_stock: - frappe.throw(_("'Update Stock' cannot be checked for fixed asset sale")) + if self.doctype != "Sales Invoice": + return - elif asset.status in ("Scrapped", "Cancelled", "Capitalized") or ( - asset.status == "Sold" and not self.is_return - ): - frappe.throw( - _("Row #{0}: Asset {1} cannot be submitted, it is already {2}").format( - d.idx, d.asset, asset.status + for d in self.get("items"): + if d.is_fixed_asset: + if d.asset: + if not self.is_return: + asset_status = frappe.db.get_value("Asset", d.asset, "status") + if self.update_stock: + frappe.throw(_("'Update Stock' cannot be checked for fixed asset sale")) + + elif asset_status in ("Scrapped", "Cancelled", "Capitalized"): + frappe.throw( + _("Row #{0}: Asset {1} cannot be sold, it is already {2}").format( + d.idx, d.asset, asset_status + ) ) + elif asset_status == "Sold" and not self.is_return: + frappe.throw(_("Row #{0}: Asset {1} is already sold").format(d.idx, d.asset)) + elif not self.return_against: + frappe.throw( + _("Row #{0}: Return Against is required for returning asset").format(d.idx) ) + else: + frappe.throw( + _("Row #{0}: You must select an Asset for Item {1}.").format(d.idx, d.item_code), + title=_("Missing Asset"), + ) def validate_item_cost_centers(self): for item in self.items: @@ -464,6 +477,8 @@ class SalesInvoice(SellingController): self.update_stock_reservation_entries() self.update_stock_ledger() + self.process_asset_depreciation() + # this sequence because outstanding may get -ve self.make_gl_entries() @@ -583,6 +598,8 @@ class SalesInvoice(SellingController): if self.update_stock == 1: self.update_stock_ledger() + self.process_asset_depreciation() + self.make_gl_entries_on_cancel() if self.update_stock == 1: @@ -1253,6 +1270,90 @@ class SalesInvoice(SellingController): ): throw(_("Delivery Note {0} is not submitted").format(d.delivery_note)) + def process_asset_depreciation(self): + if (self.is_return and self.docstatus == 2) or (not self.is_return and self.docstatus == 1): + self.depreciate_asset_on_sale() + else: + self.restore_asset() + + self.update_asset() + + def depreciate_asset_on_sale(self): + """ + Depreciate asset on sale or cancellation of return sales invoice + """ + disposal_date = self.get_disposal_date() + for d in self.get("items"): + if d.asset: + asset = frappe.get_doc("Asset", d.asset) + if asset.calculate_depreciation and asset.status != "Fully Depreciated": + depreciate_asset(asset, disposal_date, self.get_note_for_asset_sale(asset)) + + def get_note_for_asset_sale(self, asset): + return _("This schedule was created when Asset {0} was {1} through Sales Invoice {2}.").format( + get_link_to_form(asset.doctype, asset.name), + _("returned") if self.is_return else _("sold"), + get_link_to_form(self.doctype, self.get("name")), + ) + + def restore_asset(self): + """ + Restore asset on return or cancellation of original sales invoice + """ + + for d in self.get("items"): + if d.asset: + asset = frappe.get_cached_doc("Asset", d.asset) + if asset.calculate_depreciation: + reverse_depreciation_entry_made_on_disposal(asset) + + note = self.get_note_for_asset_return(asset) + reset_depreciation_schedule(asset, note) + + def get_note_for_asset_return(self, asset): + asset_link = get_link_to_form(asset.doctype, asset.name) + invoice_link = get_link_to_form(self.doctype, self.get("name")) + if self.is_return: + return _( + "This schedule was created when Asset {0} was returned through Sales Invoice {1}." + ).format(asset_link, invoice_link) + else: + return _( + "This schedule was created when Asset {0} was restored due to Sales Invoice {1} cancellation." + ).format(asset_link, invoice_link) + + def update_asset(self): + """ + Update asset status, disposal date and asset activity on sale or return sales invoice + """ + + def _update_asset(asset, disposal_date, note, asset_status=None): + frappe.db.set_value("Asset", d.asset, "disposal_date", disposal_date) + add_asset_activity(asset.name, note) + asset.set_status(asset_status) + + disposal_date = self.get_disposal_date() + for d in self.get("items"): + if d.asset: + asset = frappe.get_cached_doc("Asset", d.asset) + + if (self.is_return and self.docstatus == 1) or (not self.is_return and self.docstatus == 2): + note = _("Asset returned") if self.is_return else _("Asset sold") + asset_status, disposal_date = None, None + else: + note = _("Asset sold") if not self.is_return else _("Return invoice of asset cancelled") + asset_status = "Sold" + + _update_asset(asset, disposal_date, note, asset_status) + + def get_disposal_date(self): + if self.is_return: + disposal_date = frappe.db.get_value("Sales Invoice", self.return_against, "posting_date") + else: + disposal_date = self.posting_date + + return disposal_date + def make_gl_entries(self, gl_entries=None, from_repost=False): from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries @@ -1426,64 +1527,8 @@ class SalesInvoice(SellingController): if self.is_internal_transfer(): continue - if item.is_fixed_asset: - asset = self.get_asset(item) - - if self.is_return: - fixed_asset_gl_entries = get_gl_entries_on_asset_regain( - asset, - item.base_net_amount, - item.finance_book, - self.get("doctype"), - self.get("name"), - self.get("posting_date"), - ) - asset.db_set("disposal_date", None) - add_asset_activity(asset.name, _("Asset returned")) - - 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) - notes = _( - "This schedule was created when Asset {0} was returned 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: - if not asset.status == "Fully Depreciated": - 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( - asset, - item.base_net_amount, - item.finance_book, - self.get("doctype"), - self.get("name"), - self.get("posting_date"), - ) - asset.db_set("disposal_date", self.posting_date) - add_asset_activity(asset.name, _("Asset sold")) - - for gle in fixed_asset_gl_entries: - gle["against"] = self.customer - gl_entries.append(self.get_gl_dict(gle, item=item)) - - self.set_asset_status(asset) - + if item.is_fixed_asset and item.asset: + self.get_gl_entries_for_fixed_asset(item, gl_entries) else: income_account = ( item.income_account @@ -1518,17 +1563,31 @@ class SalesInvoice(SellingController): if cint(self.update_stock) and erpnext.is_perpetual_inventory_enabled(self.company): gl_entries += super().get_gl_entries() - def get_asset(self, item): - if item.get("asset"): - asset = frappe.get_doc("Asset", item.asset) + def get_gl_entries_for_fixed_asset(self, item, gl_entries): + asset = frappe.get_cached_doc("Asset", item.asset) + + if self.is_return: + fixed_asset_gl_entries = get_gl_entries_on_asset_regain( + asset, + item.base_net_amount, + item.finance_book, + self.get("doctype"), + self.get("name"), + self.get("posting_date"), + ) else: - frappe.throw( - _("Row #{0}: You must select an Asset for Item {1}.").format(item.idx, item.item_name), - title=_("Missing Asset"), + fixed_asset_gl_entries = get_gl_entries_on_asset_disposal( + asset, + item.base_net_amount, + item.finance_book, + self.get("doctype"), + self.get("name"), + self.get("posting_date"), ) - self.check_finance_books(item, asset) - return asset + for gle in fixed_asset_gl_entries: + gle["against"] = self.customer + gl_entries.append(self.get_gl_dict(gle, item=item)) @property def enable_discount_accounting(self): @@ -1539,12 +1598,6 @@ class SalesInvoice(SellingController): return self._enable_discount_accounting - def set_asset_status(self, asset): - if self.is_return: - asset.set_status() - else: - asset.set_status("Sold" if self.docstatus == 1 else None) - def make_loyalty_point_redemption_gle(self, gl_entries): if cint(self.redeem_loyalty_points and self.loyalty_points and not self.is_consolidated): gl_entries.append( diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 93c0fea0c4a..2e7f8868dcb 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -103,14 +103,26 @@ frappe.ui.form.on("Asset", { }, __("Manage") ); - } else if (frm.doc.status == "Scrapped") { + frm.add_custom_button( - __("Restore Asset"), + __("Repair Asset"), function () { - erpnext.asset.restore_asset(frm); + frm.trigger("create_asset_repair"); }, __("Manage") ); + + frm.add_custom_button( + __("Split Asset"), + function () { + frm.trigger("split_asset"); + }, + __("Manage") + ); + } else if (frm.doc.status == "Scrapped") { + frm.add_custom_button(__("Restore Asset"), function () { + erpnext.asset.restore_asset(frm); + }).addClass("btn-primary"); } if (frm.doc.maintenance_required && !frm.doc.maintenance_schedule) { @@ -123,23 +135,7 @@ frappe.ui.form.on("Asset", { ); } - frm.add_custom_button( - __("Repair Asset"), - function () { - frm.trigger("create_asset_repair"); - }, - __("Manage") - ); - - frm.add_custom_button( - __("Split Asset"), - function () { - frm.trigger("split_asset"); - }, - __("Manage") - ); - - if (frm.doc.status != "Fully Depreciated") { + if (in_list(["Submitted", "Partially Depreciated"], frm.doc.status)) { frm.add_custom_button( __("Adjust Asset Value"), function () { diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index a65246ee820..a5fd70402c4 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -33,8 +33,6 @@ from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_sched convert_draft_asset_depr_schedules_into_active, get_asset_depr_schedule_doc, get_depr_schedule, - make_draft_asset_depr_schedules, - update_draft_asset_depr_schedules, ) from erpnext.controllers.accounts_controller import AccountsController @@ -148,22 +146,23 @@ class Asset(AccountsController): schedule_doc = get_asset_depr_schedule_doc(self.name, "Draft", row.finance_book) if not schedule_doc: schedule_doc = frappe.new_doc("Asset Depreciation Schedule") - - schedule_doc.prepare_draft_asset_depr_schedule_data(self, row) + schedule_doc.asset = self.name + schedule_doc.create_depreciation_schedule(row) schedule_doc.save() schedules.append(schedule_doc.name) self.show_schedule_creation_message(schedules) def set_depr_rate_and_value_after_depreciation(self): + self.value_after_depreciation = flt(self.gross_purchase_amount) - flt( + self.opening_accumulated_depreciation + ) if self.calculate_depreciation: - self.value_after_depreciation = 0 self.set_depreciation_rate() + for d in self.finance_books: + d.value_after_depreciation = self.value_after_depreciation else: self.finance_books = [] - self.value_after_depreciation = flt(self.gross_purchase_amount) - flt( - self.opening_accumulated_depreciation - ) def show_schedule_creation_message(self, schedules): if schedules: @@ -845,41 +844,31 @@ class Asset(AccountsController): ) def get_written_down_value_rate(self, args, rate_field_precision, on_validate): - if ( - args.get("rate_of_depreciation") - and on_validate - and not self.flags.increase_in_asset_value_due_to_repair - ): + if args.get("rate_of_depreciation") and on_validate: return args.get("rate_of_depreciation") if args.get("rate_of_depreciation") and not flt(args.get("expected_value_after_useful_life")): return args.get("rate_of_depreciation") - if self.flags.increase_in_asset_value_due_to_repair: - value = flt(args.get("expected_value_after_useful_life")) / flt( - args.get("value_after_depreciation") - ) + if flt(args.get("value_after_depreciation")): + current_asset_value = flt(args.get("value_after_depreciation")) else: - value = flt(args.get("expected_value_after_useful_life")) / ( - flt(self.gross_purchase_amount) - flt(self.opening_accumulated_depreciation) - ) + current_asset_value = flt(self.gross_purchase_amount) - flt(self.opening_accumulated_depreciation) - depreciation_rate = math.pow( - value, - 1.0 - / ( - ( - ( - flt(args.get("total_number_of_depreciations"), 2) - - flt(self.opening_number_of_booked_depreciations) - ) - * flt(args.get("frequency_of_depreciation")) - ) - / 12 - ), + value = flt(args.get("expected_value_after_useful_life")) / current_asset_value + + pending_number_of_depreciations = ( + flt(args.get("total_number_of_depreciations"), 2) + - flt(self.opening_number_of_booked_depreciations) + - flt(args.get("total_number_of_booked_depreciations")) ) + pending_years = ( + pending_number_of_depreciations * flt(args.get("frequency_of_depreciation")) + + cint(args.get("increase_in_asset_life")) + ) / 12 - return flt((100 * (1 - depreciation_rate)), rate_field_precision) + depreciation_rate = 100 * (1 - math.pow(value, 1.0 / pending_years)) + return flt(depreciation_rate, rate_field_precision) def has_gl_entries(doctype, docname, target_account): @@ -1253,7 +1242,7 @@ def update_existing_asset(asset, remaining_qty, new_asset_name): 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) - new_asset_depr_schedule_doc.set_draft_asset_depr_schedule_details(asset, row) + new_asset_depr_schedule_doc.fetch_asset_details(asset, row) accumulated_depreciation = 0 @@ -1310,7 +1299,7 @@ def create_new_asset_after_split(asset, split_qty): continue new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc) - new_asset_depr_schedule_doc.set_draft_asset_depr_schedule_details(new_asset, row) + new_asset_depr_schedule_doc.fetch_asset_details(new_asset, row) accumulated_depreciation = 0 diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index 979c49a93eb..de7d14cb8a1 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -29,7 +29,7 @@ from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_sched 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, + reschedule_depreciation, ) @@ -136,10 +136,10 @@ def get_depreciable_asset_depr_schedules_data(date): return res -def make_depreciation_entry_for_all_asset_depr_schedules(asset_doc, date=None): +def make_depreciation_entry_on_disposal(asset_doc, disposal_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) + make_depreciation_entry(asset_depr_schedule_name, disposal_date) def get_acc_frozen_upto(): @@ -244,10 +244,12 @@ def make_depreciation_entry( except Exception as e: depreciation_posting_error = e + asset.reload() asset.set_status() if not depreciation_posting_error: asset.db_set("depr_entry_posting_status", "Successful") + asset_depr_schedule_doc.reload() return asset_depr_schedule_doc raise depreciation_posting_error @@ -316,18 +318,10 @@ def _make_journal_entry_for_depreciation( je.append("accounts", debit_entry) je.flags.ignore_permissions = True - je.flags.planned_depr_entry = True je.save() - depr_schedule.db_set("journal_entry", je.name) - if not je.meta.get_workflow(): je.submit() - asset.reload() - idx = cint(asset_depr_schedule_doc.finance_book_id) - row = asset.get("finance_books")[idx - 1] - row.value_after_depreciation -= depr_schedule.depreciation_amount - row.db_update() def get_depreciation_accounts(asset_category, company): @@ -433,194 +427,162 @@ def get_comma_separated_links(names, doctype): @frappe.whitelist() def scrap_asset(asset_name, scrap_date=None): asset = frappe.get_doc("Asset", asset_name) + scrap_date = getdate(scrap_date) or getdate(today()) + asset.db_set("disposal_date", scrap_date) + validate_asset_for_scrap(asset, scrap_date) + depreciate_asset(asset, scrap_date, get_note_for_scrap(asset)) + asset.reload() + + create_journal_entry_for_scrap(asset, scrap_date) + + +def validate_asset_for_scrap(asset, scrap_date): if asset.docstatus != 1: frappe.throw(_("Asset {0} must be submitted").format(asset.name)) elif asset.status in ("Cancelled", "Sold", "Scrapped", "Capitalized"): frappe.throw(_("Asset {0} cannot be scrapped, as it is already {1}").format(asset.name, asset.status)) - today_date = getdate(today()) - date = getdate(scrap_date) or today_date - purchase_date = getdate(asset.purchase_date) + validate_scrap_date(asset, scrap_date) - validate_scrap_date(date, today_date, purchase_date, asset.calculate_depreciation, asset_name) - notes = _("This schedule was created when Asset {0} was scrapped.").format( +def validate_scrap_date(asset, scrap_date): + if scrap_date > getdate(): + frappe.throw(_("Future date is not allowed")) + elif scrap_date < getdate(asset.purchase_date): + frappe.throw(_("Scrap date cannot be before purchase date")) + + if asset.calculate_depreciation: + last_booked_depreciation_date = get_last_depreciation_date(asset.name) + if ( + last_booked_depreciation_date + and scrap_date < last_booked_depreciation_date + and scrap_date > getdate(asset.purchase_date) + ): + frappe.throw(_("Asset cannot be scrapped before the last depreciation entry.")) + + +def get_last_depreciation_date(asset_name): + depreciation = frappe.qb.DocType("Asset Depreciation Schedule") + depreciation_schedule = frappe.qb.DocType("Depreciation Schedule") + + last_depreciation_date = ( + frappe.qb.from_(depreciation) + .join(depreciation_schedule) + .on(depreciation.name == depreciation_schedule.parent) + .select(depreciation_schedule.schedule_date) + .where(depreciation.asset == asset_name) + .where(depreciation.docstatus == 1) + .where(depreciation_schedule.journal_entry != "") + .orderby(depreciation_schedule.schedule_date, order=Order.desc) + .limit(1) + .run() + ) + + return last_depreciation_date[0][0] if last_depreciation_date else None + + +def get_note_for_scrap(asset): + return _("This schedule was created when Asset {0} was scrapped.").format( get_link_to_form(asset.doctype, asset.name) ) - if asset.status != "Fully Depreciated": - depreciate_asset(asset, date, notes) - asset.reload() + +def create_journal_entry_for_scrap(asset, scrap_date): depreciation_series = frappe.get_cached_value("Company", asset.company, "series_for_depreciation_entry") je = frappe.new_doc("Journal Entry") - je.voucher_type = "Journal Entry" + je.voucher_type = "Asset Disposal" je.naming_series = depreciation_series - je.posting_date = date + je.posting_date = scrap_date je.company = asset.company - je.remark = f"Scrap Entry for asset {asset_name}" + je.remark = f"Scrap Entry for asset {asset.name}" - for entry in get_gl_entries_on_asset_disposal(asset, date): - entry.update({"reference_type": "Asset", "reference_name": asset_name}) + for entry in get_gl_entries_on_asset_disposal(asset, scrap_date): + entry.update({"reference_type": "Asset", "reference_name": asset.name}) je.append("accounts", entry) je.flags.ignore_permissions = True - je.submit() + je.save() + if not je.meta.get_workflow(): + je.submit() - frappe.db.set_value("Asset", asset_name, "disposal_date", date) - frappe.db.set_value("Asset", asset_name, "journal_entry_for_scrap", je.name) - asset.set_status("Scrapped") - - add_asset_activity(asset_name, _("Asset scrapped")) - - frappe.msgprint(_("Asset scrapped via Journal Entry {0}").format(je.name)) - - -def validate_scrap_date(scrap_date, today_date, purchase_date, calculate_depreciation, asset_name): - if scrap_date > today_date: - frappe.throw(_("Future date is not allowed")) - elif scrap_date < purchase_date: - frappe.throw(_("Scrap date cannot be before purchase date")) - - if calculate_depreciation: - asset_depreciation_schedules = frappe.db.get_all( - "Asset Depreciation Schedule", filters={"asset": asset_name, "docstatus": 1}, fields=["name"] - ) - - for depreciation_schedule in asset_depreciation_schedules: - last_booked_depreciation_date = frappe.db.get_value( - "Depreciation Schedule", - { - "parent": depreciation_schedule["name"], - "docstatus": 1, - "journal_entry": ["!=", ""], - }, - "schedule_date", - order_by="schedule_date desc", - ) - if ( - last_booked_depreciation_date - and scrap_date < last_booked_depreciation_date - and scrap_date > purchase_date - ): - frappe.throw(_("Asset cannot be scrapped before the last depreciation entry.")) + add_asset_activity(asset.name, _("Asset scrapped")) + frappe.msgprint( + _("Asset scrapped via Journal Entry {0}").format(get_link_to_form("Journal Entry", je.name)) + ) @frappe.whitelist() def restore_asset(asset_name): asset = frappe.get_doc("Asset", asset_name) + reverse_depreciation_entry_made_on_disposal(asset) + reset_depreciation_schedule(asset, get_note_for_restore(asset)) + cancel_journal_entry_for_scrap(asset) + asset.set_status() + add_asset_activity(asset_name, _("Asset restored")) - reverse_depreciation_entry_made_after_disposal(asset, asset.disposal_date) - je = asset.journal_entry_for_scrap - - notes = _("This schedule was created when Asset {0} was restored.").format( +def get_note_for_restore(asset): + return _("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) - - frappe.get_doc("Journal Entry", je).cancel() - - asset.set_status() - - add_asset_activity(asset_name, _("Asset restored")) +def cancel_journal_entry_for_scrap(asset): + if asset.journal_entry_for_scrap: + je = asset.journal_entry_for_scrap + asset.db_set("disposal_date", None) + asset.db_set("journal_entry_for_scrap", None) + frappe.get_doc("Journal Entry", je).cancel() def depreciate_asset(asset_doc, date, notes): if not asset_doc.calculate_depreciation: return - asset_doc.flags.ignore_validate_update_after_submit = True - - 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) + reschedule_depreciation(asset_doc, notes, disposal_date=date) + make_depreciation_entry_on_disposal(asset_doc, date) + # As per Income Tax Act (India), the asset should not be depreciated + # in the financial year in which it is sold/scraped asset_doc.reload() - cancel_depreciation_entries(asset_doc, date) + # cancel_depreciation_entries(asset_doc, date) @erpnext.allow_regional def cancel_depreciation_entries(asset_doc, date): + # Cancel all depreciation entries for the current financial year + # if the asset is sold/scraped in the current financial year + # Overwritten via India Compliance app pass -def reset_depreciation_schedule(asset_doc, date, notes): - if not asset_doc.calculate_depreciation: - return - - asset_doc.flags.ignore_validate_update_after_submit = True - - make_new_active_asset_depr_schedules_and_cancel_current_ones(asset_doc, notes, date_of_return=date) - - modify_depreciation_schedule_for_asset_repairs(asset_doc, notes) - - asset_doc.save() +def reset_depreciation_schedule(asset_doc, notes): + if asset_doc.calculate_depreciation: + reschedule_depreciation(asset_doc, notes) -def modify_depreciation_schedule_for_asset_repairs(asset, notes): - asset_repairs = frappe.get_all( - "Asset Repair", filters={"asset": asset.name}, fields=["name", "increase_in_asset_life"] - ) - - for repair in asset_repairs: - if repair.increase_in_asset_life: - asset_repair = frappe.get_doc("Asset Repair", repair.name) - asset_repair.modify_depreciation_schedule() - make_new_active_asset_depr_schedules_and_cancel_current_ones(asset, notes) - - -def reverse_depreciation_entry_made_after_disposal(asset, date): +def reverse_depreciation_entry_made_on_disposal(asset): for row in asset.get("finance_books"): - asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active", row.finance_book) - if not asset_depr_schedule_doc or not asset_depr_schedule_doc.get("depreciation_schedule"): + schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active", row.finance_book) + if not schedule_doc or not schedule_doc.get("depreciation_schedule"): continue - for schedule_idx, schedule in enumerate(asset_depr_schedule_doc.get("depreciation_schedule")): - if schedule.schedule_date == date and schedule.journal_entry: + for schedule_idx, schedule in enumerate(schedule_doc.get("depreciation_schedule")): + if schedule.schedule_date == asset.disposal_date and schedule.journal_entry: 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() - - for account in reverse_journal_entry.accounts: - account.update( - { - "reference_type": "Asset", - "reference_name": asset.name, - } - ) - - frappe.flags.is_reverse_depr_entry = True - reverse_journal_entry.submit() - - 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() + schedule_idx, row, asset.disposal_date + ) or disposal_happens_in_the_future(asset.disposal_date): + je = create_reverse_depreciation_entry(asset.name, schedule.journal_entry) + update_value_after_depreciation_on_asset_restore(schedule, row, je) -def get_depreciation_amount_in_je(journal_entry): - if journal_entry.accounts[0].debit_in_account_currency: - return journal_entry.accounts[0].debit_in_account_currency - else: - return journal_entry.accounts[0].credit_in_account_currency - - -# if the invoice had been posted on the date the depreciation was initially supposed to happen, the depreciation shouldn't be undone -def disposal_was_made_on_original_schedule_date(schedule_idx, row, posting_date_of_disposal): +def disposal_was_made_on_original_schedule_date(schedule_idx, row, disposal_date): + """ + If asset is scrapped or sold on original schedule date, + then the depreciation entry should not be reversed. + """ orginal_schedule_date = add_months( row.depreciation_start_date, schedule_idx * cint(row.frequency_of_depreciation) ) @@ -628,19 +590,57 @@ def disposal_was_made_on_original_schedule_date(schedule_idx, row, posting_date_ if is_last_day_of_the_month(row.depreciation_start_date): orginal_schedule_date = get_last_day(orginal_schedule_date) - if orginal_schedule_date == posting_date_of_disposal: + if orginal_schedule_date == disposal_date: return True return False -def disposal_happens_in_the_future(posting_date_of_disposal): - if posting_date_of_disposal > getdate(): +def disposal_happens_in_the_future(disposal_date): + if disposal_date > getdate(): return True return False +def create_reverse_depreciation_entry(asset_name, journal_entry): + reverse_journal_entry = make_reverse_journal_entry(journal_entry) + reverse_journal_entry.posting_date = nowdate() + + for account in reverse_journal_entry.accounts: + account.update( + { + "reference_type": "Asset", + "reference_name": asset_name, + } + ) + + frappe.flags.is_reverse_depr_entry = True + if not reverse_journal_entry.meta.get_workflow(): + reverse_journal_entry.submit() + return reverse_journal_entry + else: + frappe.throw( + _("Please disable workflow temporarily for Journal Entry {0}").format(reverse_journal_entry.name) + ) + + +def update_value_after_depreciation_on_asset_restore(schedule, row, journal_entry): + frappe.db.set_value("Depreciation Schedule", schedule.name, "journal_entry", None, update_modified=False) + depreciation_amount = get_depreciation_amount_in_je(journal_entry) + value_after_depreciation = flt( + row.value_after_depreciation + depreciation_amount, row.precision("value_after_depreciation") + ) + row.db_set("value_after_depreciation", value_after_depreciation) + + +def get_depreciation_amount_in_je(journal_entry): + if journal_entry.accounts[0].debit_in_account_currency: + return journal_entry.accounts[0].debit_in_account_currency + else: + return journal_entry.accounts[0].credit_in_account_currency + + def get_gl_entries_on_asset_regain( asset, selling_amount=0, finance_book=None, voucher_type=None, voucher_no=None, date=None ): diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index be8ce0ff5fc..7162e8f5a1e 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -35,7 +35,7 @@ from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_sched get_asset_depr_schedule_doc, get_depr_schedule, ) -from erpnext.assets.doctype.asset_depreciation_schedule.utils import ( +from erpnext.erpnext.assets.doctype.asset_depreciation_schedule.deppreciation_schedule_controller import ( get_depreciation_amount, ) from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index e01c722b0d4..449fffc0684 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -16,7 +16,7 @@ from erpnext.assets.doctype.asset.depreciation import ( get_gl_entries_on_asset_disposal, get_value_after_depreciation_on_disposal_date, reset_depreciation_schedule, - reverse_depreciation_entry_made_after_disposal, + reverse_depreciation_entry_made_on_disposal, ) from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account @@ -623,13 +623,13 @@ class AssetCapitalization(StockController): self.set_consumed_asset_status(asset) if asset.calculate_depreciation: - reverse_depreciation_entry_made_after_disposal(asset, self.posting_date) + reverse_depreciation_entry_made_on_disposal(asset, self.posting_date) notes = _( "This schedule was created when Asset {0} was restored on Asset Capitalization {1}'s cancellation." ).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) + reset_depreciation_schedule(asset, notes) def set_consumed_asset_status(self, asset): if self.docstatus == 1: diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py index 4566f52b20a..c75e08bbb87 100644 --- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py @@ -3,7 +3,6 @@ import frappe from frappe import _ -from frappe.model.document import Document from frappe.utils import ( add_days, add_months, @@ -20,13 +19,12 @@ from frappe.utils import ( ) import erpnext -from erpnext.accounts.utils import get_fiscal_year -from erpnext.assets.doctype.asset_depreciation_schedule.utils import ( - get_depreciation_amount, +from erpnext.assets.doctype.asset_depreciation_schedule.deppreciation_schedule_controller import ( + DepreciationScheduleController, ) -class AssetDepreciationSchedule(Document): +class AssetDepreciationSchedule(DepreciationScheduleController): # begin: auto-generated types # This code is auto-generated. Do not modify anything in this block. @@ -61,15 +59,11 @@ class AssetDepreciationSchedule(Document): value_after_depreciation: DF.Currency # end: auto-generated types - 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 - ) - self.update_shift_depr_schedule() - def validate(self): self.validate_another_asset_depr_schedule_does_not_exist() + if not self.finance_book_id: + self.create_depreciation_schedule() + self.update_shift_depr_schedule() def validate_another_asset_depr_schedule_does_not_exist(self): finance_book_filter = ["finance_book", "is", "not set"] @@ -102,7 +96,8 @@ class AssetDepreciationSchedule(Document): def on_submit(self): self.db_set("status", "Active") - def before_cancel(self): + def on_cancel(self): + self.db_set("status", "Cancelled") if not self.flags.should_not_cancel_depreciation_entries: self.cancel_depreciation_entries() @@ -111,9 +106,6 @@ class AssetDepreciationSchedule(Document): if d.journal_entry: frappe.get_doc("Journal Entry", d.journal_entry).cancel() - def on_cancel(self): - self.db_set("status", "Cancelled") - def update_shift_depr_schedule(self): if not self.shift_based or self.docstatus != 0: return @@ -124,594 +116,50 @@ class AssetDepreciationSchedule(Document): self.make_depr_schedule(asset_doc, fb_row) self.set_accumulated_depreciation(asset_doc, fb_row) - 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) + def get_finance_book_row(self, fb_row=None): + if fb_row: + self.fb_row = fb_row + return finance_book_filter = ["finance_book", "is", "not set"] - if fb_name: - finance_book_filter = ["finance_book", "=", fb_name] + if self.finance_book: + finance_book_filter = ["finance_book", "=", self.finance_book] asset_finance_book_name = frappe.db.get_value( doctype="Asset Finance Book", - filters=[["parent", "=", asset_name], finance_book_filter], + filters=[["parent", "=", self.asset], finance_book_filter], ) - asset_finance_book_doc = frappe.get_doc("Asset Finance Book", asset_finance_book_name) + self.fb_row = frappe.get_doc("Asset Finance Book", asset_finance_book_name) - self.prepare_draft_asset_depr_schedule_data(asset_doc, asset_finance_book_doc) - - def prepare_draft_asset_depr_schedule_data( - self, - asset_doc, - row, - date_of_disposal=None, - date_of_return=None, - update_asset_finance_book_row=True, - ): - self.set_draft_asset_depr_schedule_details(asset_doc, row) - self.make_depr_schedule(asset_doc, row, date_of_disposal, update_asset_finance_book_row) - self.set_accumulated_depreciation(asset_doc, row, date_of_disposal, date_of_return) - - def set_draft_asset_depr_schedule_details(self, asset_doc, row): - self.asset = asset_doc.name - self.finance_book = row.finance_book - self.finance_book_id = row.idx - self.opening_accumulated_depreciation = asset_doc.opening_accumulated_depreciation or 0 - self.opening_number_of_booked_depreciations = asset_doc.opening_number_of_booked_depreciations or 0 - self.gross_purchase_amount = asset_doc.gross_purchase_amount - self.depreciation_method = row.depreciation_method - self.total_number_of_depreciations = row.total_number_of_depreciations - self.frequency_of_depreciation = row.frequency_of_depreciation - self.rate_of_depreciation = row.rate_of_depreciation - self.expected_value_after_useful_life = row.expected_value_after_useful_life - self.daily_prorata_based = row.daily_prorata_based - self.shift_based = row.shift_based + def fetch_asset_details(self): + self.asset = self.asset_doc.name + self.finance_book = self.fb_row.get("finance_book") + self.finance_book_id = self.fb_row.idx + self.opening_accumulated_depreciation = self.asset_doc.opening_accumulated_depreciation or 0 + self.opening_number_of_booked_depreciations = ( + self.asset_doc.opening_number_of_booked_depreciations or 0 + ) + self.gross_purchase_amount = self.asset_doc.gross_purchase_amount + self.depreciation_method = self.fb_row.depreciation_method + self.total_number_of_depreciations = self.fb_row.total_number_of_depreciations + self.frequency_of_depreciation = self.fb_row.frequency_of_depreciation + self.rate_of_depreciation = self.fb_row.get("rate_of_depreciation") + self.expected_value_after_useful_life = self.fb_row.get("expected_value_after_useful_life") + self.daily_prorata_based = self.fb_row.get("daily_prorata_based") + self.shift_based = self.fb_row.get("shift_based") self.status = "Draft" - def make_depr_schedule( - self, - asset_doc, - row, - date_of_disposal=None, - update_asset_finance_book_row=True, - value_after_depreciation=None, - ): - start = self.clear_depr_schedule() - - self._make_depr_schedule( - asset_doc, row, start, date_of_disposal, update_asset_finance_book_row, value_after_depreciation - ) - - def clear_depr_schedule(self): - """ - Clears the depreciation schedule preserving the depreciation entries that have been booked. - """ - start = 0 - num_of_depreciations_completed = 0 - depr_schedule = [] - - self.schedules_before_clearing = self.get("depreciation_schedule") - - for schedule in self.get("depreciation_schedule"): - if schedule.journal_entry: - num_of_depreciations_completed += 1 - depr_schedule.append(schedule) - else: - start = num_of_depreciations_completed - break - - self.depreciation_schedule = depr_schedule - - return start - - def _make_depr_schedule( - self, - asset_doc, - row, - start, - date_of_disposal, - update_asset_finance_book_row, - value_after_depreciation, - ): - row, value_after_depreciation = self.get_value_after_depreciation( - asset_doc, row, value_after_depreciation, update_asset_finance_book_row - ) - final_number_of_depreciations, has_pro_rata = self.get_final_number_of_depreciations(asset_doc, row) - has_wdv_or_dd_non_yearly_pro_rata = self.is_wdv_or_dd_non_yearly_pro_rata(asset_doc, row) - should_get_last_day = is_last_day_of_the_month(row.depreciation_start_date) - - skip_row = False - depreciation_amount = 0 - prev_per_day_depr = True - self.current_fiscal_year_end_date = None - yearly_opening_wdv = value_after_depreciation - pending_months = self.get_number_of_pending_months(asset_doc, row, start) - - for n in range(start, final_number_of_depreciations): - # If depreciation is already completed (for double declining balance) - if skip_row: - continue - - if self.has_fiscal_year_changed(row, n): - yearly_opening_wdv = value_after_depreciation - - prev_depreciation_amount = self.get_prev_depreciation_amount(n) - - depreciation_amount, prev_per_day_depr = get_depreciation_amount( - self, - asset_doc, - value_after_depreciation, - yearly_opening_wdv, - row, - n, - prev_depreciation_amount, - has_wdv_or_dd_non_yearly_pro_rata, - pending_months, - prev_per_day_depr, - ) - - schedule_date = self.get_next_schedule_date( - row, n, has_pro_rata, should_get_last_day, final_number_of_depreciations - ) - - # if asset is being sold or scrapped - if date_of_disposal and getdate(schedule_date) >= getdate(date_of_disposal): - self.get_depreciation_amount_for_disposal( - asset_doc, row, n, schedule_date, date_of_disposal, depreciation_amount - ) - break - - if n == 0: - # Get pro rata amount for first row if available for use date is mid of the month - depreciation_amount = self.get_depreciation_amount_for_first_row( - asset_doc, row, n, depreciation_amount, has_pro_rata, has_wdv_or_dd_non_yearly_pro_rata - ) - elif has_pro_rata and n == cint(final_number_of_depreciations) - 1: # for the last row - depreciation_amount, schedule_date = self.get_depreciation_amount_for_last_row( - asset_doc, row, n, depreciation_amount, schedule_date, has_wdv_or_dd_non_yearly_pro_rata - ) - - if not depreciation_amount: - break - - - value_after_depreciation = flt( - value_after_depreciation - flt(depreciation_amount, asset_doc.precision("gross_purchase_amount")), - asset_doc.precision("gross_purchase_amount"), - ) - - # Adjust depreciation amount in the last period based on the expected value after useful life - depreciation_amount, skip_row = self.adjust_depr_amount_for_salvage_value( - row, depreciation_amount, value_after_depreciation, final_number_of_depreciations, n, skip_row - ) - - if flt(depreciation_amount, asset_doc.precision("gross_purchase_amount")) > 0: - self.add_depr_schedule_row(schedule_date, depreciation_amount, n) - - def get_value_after_depreciation( - self, asset_doc, row, value_after_depreciation, update_asset_finance_book_row - ): - if not value_after_depreciation: - value_after_depreciation = _get_value_after_depreciation_for_making_schedule(asset_doc, row) - row.value_after_depreciation = value_after_depreciation - - if update_asset_finance_book_row: - row.db_update() - - return row, value_after_depreciation - - def get_final_number_of_depreciations(self, asset_doc, row): - final_number_of_depreciations = cint(row.total_number_of_depreciations) - cint( - self.opening_number_of_booked_depreciations - ) - - has_pro_rata = _check_is_pro_rata(asset_doc, row) - if has_pro_rata: - final_number_of_depreciations += 1 - - if row.increase_in_asset_life: - final_number_of_depreciations = ( - self.get_final_number_of_depreciations_considering_increase_in_asset_life( - asset_doc, row, final_number_of_depreciations - ) - ) - - return final_number_of_depreciations, has_pro_rata - - def get_final_number_of_depreciations_considering_increase_in_asset_life( - self, asset_doc, row, final_number_of_depreciations - ): - # final schedule date after increasing asset life - self.final_schedule_date = add_months( - asset_doc.available_for_use_date, - (row.total_number_of_depreciations * cint(row.frequency_of_depreciation)) - + row.increase_in_asset_life, - ) - - number_of_pending_depreciations = cint(row.total_number_of_depreciations) - cint( - asset_doc.opening_number_of_booked_depreciations - ) - schedule_date = add_months( - row.depreciation_start_date, - number_of_pending_depreciations * cint(row.frequency_of_depreciation), - ) - - if self.final_schedule_date > schedule_date: - final_number_of_depreciations += 1 - - return final_number_of_depreciations - - def is_wdv_or_dd_non_yearly_pro_rata(self, asset_doc, row): - has_wdv_or_dd_non_yearly_pro_rata = False - if ( - row.depreciation_method in ("Written Down Value", "Double Declining Balance") - and cint(row.frequency_of_depreciation) != 12 - ): - has_wdv_or_dd_non_yearly_pro_rata = _check_is_pro_rata(asset_doc, row, wdv_or_dd_non_yearly=True) - - return has_wdv_or_dd_non_yearly_pro_rata - - def get_number_of_pending_months(self, asset_doc, row, start): - total_months = cint(row.total_number_of_depreciations) * cint(row.frequency_of_depreciation) + cint( - row.increase_in_asset_life - ) - depr_booked_for_months = 0 - last_depr_date = None - if start > 0: - last_depr_date = self.depreciation_schedule[start - 1].schedule_date - elif asset_doc.opening_number_of_booked_depreciations > 0: - last_depr_date = add_months(row.depreciation_start_date, -1 * row.frequency_of_depreciation) - - if last_depr_date: - depr_booked_for_months = date_diff(last_depr_date, asset_doc.available_for_use_date) / (365 / 12) - - return total_months - depr_booked_for_months - - def has_fiscal_year_changed(self, row, row_no): - fiscal_year_changed = False - - schedule_date = get_last_day( - add_months(row.depreciation_start_date, row_no * cint(row.frequency_of_depreciation)) - ) - - if not self.current_fiscal_year_end_date: - self.current_fiscal_year_end_date = get_fiscal_year(row.depreciation_start_date)[2] - fiscal_year_changed = True - elif getdate(schedule_date) > getdate(self.current_fiscal_year_end_date): - self.current_fiscal_year_end_date = add_years(self.current_fiscal_year_end_date, 1) - fiscal_year_changed = True - - return fiscal_year_changed - - def get_prev_depreciation_amount(self, n): - prev_depreciation_amount = 0 - if n > 0 and len(self.get("depreciation_schedule")) > n - 1: - prev_depreciation_amount = self.get("depreciation_schedule")[n - 1].depreciation_amount - - return prev_depreciation_amount - - def get_next_schedule_date( - self, row, n, has_pro_rata, should_get_last_day, final_number_of_depreciations=None - ): - 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) - - return schedule_date - - def get_depreciation_amount_for_disposal( - self, asset_doc, row, row_no, schedule_date, date_of_disposal, depreciation_amount - ): - if self.depreciation_schedule: # if there are already booked depreciations - from_date = add_days(self.depreciation_schedule[-1].schedule_date, 1) - else: - from_date = _get_modified_available_for_use_date_for_existing_assets(asset_doc, row) - if is_last_day_of_the_month(getdate(asset_doc.available_for_use_date)): - from_date = get_last_day(from_date) - - depreciation_amount, days, months = _get_pro_rata_amt( - row, - depreciation_amount, - from_date, - date_of_disposal, - original_schedule_date=schedule_date, - ) - - depreciation_amount = flt(depreciation_amount, asset_doc.precision("gross_purchase_amount")) - if depreciation_amount > 0: - self.add_depr_schedule_row(date_of_disposal, depreciation_amount, row_no) - - def get_adjusted_depreciation_amount( - self, depreciation_amount_without_pro_rata, depreciation_amount_for_last_row - ): - # to ensure that final accumulated depreciation amount is accurate - if not self.opening_accumulated_depreciation: - depreciation_amount_for_first_row = self.get("depreciation_schedule")[0].depreciation_amount - - 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, asset_doc, row, n, depreciation_amount, has_pro_rata, has_wdv_or_dd_non_yearly_pro_rata - ): - """ - For the first row, if available for use date is mid of the month, then pro rata amount is needed - """ - pro_rata_amount_applicable = False - if ( - (has_pro_rata or has_wdv_or_dd_non_yearly_pro_rata) - and not self.opening_accumulated_depreciation - and not self.flags.wdv_it_act_applied - ): # if not existing asset - from_date = asset_doc.available_for_use_date - pro_rata_amount_applicable = True - elif has_wdv_or_dd_non_yearly_pro_rata and self.opening_accumulated_depreciation: # if existing asset - from_date = _get_modified_available_for_use_date_for_existing_assets(asset_doc, row) - pro_rata_amount_applicable = True - - if pro_rata_amount_applicable: - depreciation_amount, days, months = _get_pro_rata_amt( - row, - depreciation_amount, - from_date, - row.depreciation_start_date, - has_wdv_or_dd_non_yearly_pro_rata, - ) - - self.validate_depreciation_amount_for_low_value_assets(asset_doc, row, depreciation_amount) - - return depreciation_amount - - def get_depreciation_amount_for_last_row( - self, asset_doc, row, n, depreciation_amount, schedule_date, has_wdv_or_dd_non_yearly_pro_rata - ): - if not row.increase_in_asset_life: - # In case of increase_in_asset_life, the asset.to_date is already set on asset_repair submission - self.final_schedule_date = add_months( - asset_doc.available_for_use_date, - (n + self.opening_number_of_booked_depreciations) * cint(row.frequency_of_depreciation), - ) - if is_last_day_of_the_month(getdate(asset_doc.available_for_use_date)): - self.final_schedule_date = get_last_day(self.final_schedule_date) - - if self.opening_accumulated_depreciation: - depreciation_amount, days, months = _get_pro_rata_amt( - row, - depreciation_amount, - schedule_date, - self.final_schedule_date, - has_wdv_or_dd_non_yearly_pro_rata, - ) - else: - # if not existing asset, remaining amount of first row is depreciated in the last row - if not row.increase_in_asset_life: - depreciation_amount -= self.get("depreciation_schedule")[0].depreciation_amount - days = date_diff(self.final_schedule_date, schedule_date) + 1 - - schedule_date = add_days(schedule_date, days - 1) - return depreciation_amount, schedule_date - - def adjust_depr_amount_for_salvage_value( - self, - row, - depreciation_amount, - value_after_depreciation, - final_number_of_depreciations, - row_no, - skip_row, - ): - if ( - row_no == cint(final_number_of_depreciations) - 1 - and flt(value_after_depreciation) != flt(row.expected_value_after_useful_life) - ) or flt(value_after_depreciation) < flt(row.expected_value_after_useful_life): - depreciation_amount += flt(value_after_depreciation) - flt(row.expected_value_after_useful_life) - depreciation_amount = flt(depreciation_amount, row.precision("depreciation_amount")) - skip_row = True - return depreciation_amount, skip_row - - def validate_depreciation_amount_for_low_value_assets(self, asset_doc, row, depreciation_amount): - """ - If gross purchase amount is too low, then depreciation amount - can come zero sometimes based on the frequency and number of depreciations. - """ - if flt(depreciation_amount, asset_doc.precision("gross_purchase_amount")) <= 0: - frappe.throw( - _("Gross Purchase Amount {0} cannot be depreciated over {1} cycles.").format( - frappe.bold(asset_doc.gross_purchase_amount), - frappe.bold(row.total_number_of_depreciations), - ) - ) - - def add_depr_schedule_row(self, schedule_date, depreciation_amount, schedule_idx): - if self.shift_based: - shift = ( - self.schedules_before_clearing[schedule_idx].shift - if self.schedules_before_clearing and len(self.schedules_before_clearing) > schedule_idx - else frappe.get_cached_value("Asset Shift Factor", {"default": 1}, "shift_name") - ) - else: - shift = None - - self.append( - "depreciation_schedule", - { - "schedule_date": schedule_date, - "depreciation_amount": depreciation_amount, - "shift": shift, - }, - ) - - def set_accumulated_depreciation( - self, - asset_doc, - row, - date_of_disposal=None, - date_of_return=None, - ): - accumulated_depreciation = flt(self.opening_accumulated_depreciation) - value_after_depreciation = flt(row.value_after_depreciation) - - for i, d in enumerate(self.get("depreciation_schedule")): - if d.journal_entry: - accumulated_depreciation = d.accumulated_depreciation_amount - continue - - value_after_depreciation = flt( - value_after_depreciation - flt(d.depreciation_amount), d.precision("depreciation_amount") - ) - - # for the last row, if depreciation method = Straight Line - if ( - self.depreciation_method in ("Straight Line", "Manual") - and i == len(self.get("depreciation_schedule")) - 1 - and not date_of_disposal - and not date_of_return - and not row.shift_based - ): - d.depreciation_amount += flt( - value_after_depreciation - flt(row.expected_value_after_useful_life), - d.precision("depreciation_amount"), - ) - - accumulated_depreciation += d.depreciation_amount - d.accumulated_depreciation_amount = flt( - accumulated_depreciation, d.precision("accumulated_depreciation_amount") - ) - - -def _get_value_after_depreciation_for_making_schedule(asset_doc, fb_row): - if asset_doc.docstatus == 1 and fb_row.value_after_depreciation: - value_after_depreciation = flt(fb_row.value_after_depreciation) - else: - value_after_depreciation = flt(asset_doc.gross_purchase_amount) - flt( - asset_doc.opening_accumulated_depreciation - ) - - return value_after_depreciation - - -# if it returns True, depreciation_amount will not be equal for the first and last rows -def _check_is_pro_rata(asset_doc, row, wdv_or_dd_non_yearly=False): - has_pro_rata = False - - # if not existing asset, from_date = available_for_use_date - # otherwise, if opening_number_of_booked_depreciations = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12 - # from_date = 01/01/2022 - if row.depreciation_method in ("Straight Line", "Manual"): - prev_depreciation_start_date = get_last_day( - add_months( - row.depreciation_start_date, - (row.frequency_of_depreciation * -1) * asset_doc.opening_number_of_booked_depreciations, - ) - ) - from_date = asset_doc.available_for_use_date - days = date_diff(prev_depreciation_start_date, from_date) + 1 - total_days = get_total_days(prev_depreciation_start_date, row.frequency_of_depreciation) - else: - from_date = _get_modified_available_for_use_date_for_existing_assets(asset_doc, row) - days = date_diff(row.depreciation_start_date, from_date) + 1 - total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation) - if days <= 0: - frappe.throw( - _( - """Error: This asset already has {0} depreciation periods booked. - The `depreciation start` date must be at least {1} periods after the `available for use` date. - Please correct the dates accordingly.""" - ).format( - asset_doc.opening_number_of_booked_depreciations, - asset_doc.opening_number_of_booked_depreciations, - ) - ) - if days < total_days: - has_pro_rata = True - return has_pro_rata - - -def _get_modified_available_for_use_date_for_existing_assets(asset_doc, row): - """ - if Asset has opening booked depreciations = 3, - frequency of depreciation = 3, - available for use date = 17-07-2023, - depreciation start date = 30-06-2024 - then from date should be 01-04-2024 - """ - if asset_doc.opening_number_of_booked_depreciations > 0: - from_date = add_months( - asset_doc.available_for_use_date, - (asset_doc.opening_number_of_booked_depreciations * row.frequency_of_depreciation) - 1, - ) - if is_last_day_of_the_month(row.depreciation_start_date): - return add_days(get_last_day(from_date), 1) - - # get from date when depreciation start date is not last day of the month - months_difference = month_diff(row.depreciation_start_date, from_date) - 1 - return add_days(add_months(row.depreciation_start_date, -1 * months_difference), 1) - else: - return asset_doc.available_for_use_date - - -def _get_pro_rata_amt( - row, - depreciation_amount, - from_date, - to_date, - has_wdv_or_dd_non_yearly_pro_rata=False, - original_schedule_date=None, -): - days = date_diff(to_date, from_date) + 1 - months = month_diff(to_date, from_date) - if has_wdv_or_dd_non_yearly_pro_rata: - total_days = get_total_days(original_schedule_date or to_date, 12) - else: - total_days = get_total_days(original_schedule_date or to_date, row.frequency_of_depreciation) - return (depreciation_amount * flt(days)) / flt(total_days), days, months - - -def get_total_days(date, frequency): - period_start_date = add_months(date, cint(frequency) * -1) - if is_last_day_of_the_month(date): - period_start_date = get_last_day(period_start_date) - return date_diff(date, period_start_date) - - -def make_draft_asset_depr_schedules(asset_doc): - asset_depr_schedules_names = [] - - for row in asset_doc.get("finance_books"): - name = make_draft_asset_depr_schedule(asset_doc, row) - asset_depr_schedules_names.append(name) - - return asset_depr_schedules_names - def make_draft_asset_depr_schedule(asset_doc, row): asset_depr_schedule_doc = frappe.new_doc("Asset Depreciation Schedule") - asset_depr_schedule_doc.prepare_draft_asset_depr_schedule_data(asset_doc, row) + asset_depr_schedule_doc.create_depreciation_schedule(asset_doc, row) asset_depr_schedule_doc.insert() return asset_depr_schedule_doc.name -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 - - asset_depr_schedule_doc.prepare_draft_asset_depr_schedule_data(asset_doc, row) - - asset_depr_schedule_doc.save() - - 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) @@ -732,74 +180,63 @@ def cancel_asset_depr_schedules(asset_doc): 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, - value_after_depreciation=None, - difference_amount=None, -): +def reschedule_depreciation(asset_doc, notes, disposal_date=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 + current_schedule = get_asset_depr_schedule_doc(asset_doc.name, None, row.finance_book) + + if current_schedule: + if current_schedule.docstatus == 1: + new_schedule = frappe.copy_doc(current_schedule) + elif current_schedule.docstatus == 0: + new_schedule = current_schedule + else: + new_schedule = frappe.new_doc("Asset Depreciation Schedule") + new_schedule.asset = asset_doc.name + + set_modified_depreciation_rate(asset_doc, row, new_schedule) + + new_schedule.create_depreciation_schedule(row, disposal_date) + new_schedule.notes = notes + + if current_schedule and current_schedule.docstatus == 1: + current_schedule.flags.should_not_cancel_depreciation_entries = True + current_schedule.cancel() + + new_schedule.submit() + + +def set_modified_depreciation_rate(asset_doc, row, new_schedule): + if row.depreciation_method in ( + "Written Down Value", + "Double Declining Balance", + ): + new_rate_of_depreciation = flt( + asset_doc.get_depreciation_rate(row), row.precision("rate_of_depreciation") ) - if not current_asset_depr_schedule_doc: - frappe.throw( - _("Asset Depreciation Schedule not found for Asset {0} and Finance Book {1}").format( - get_link_to_form("Asset", asset_doc.name), row.finance_book - ) - ) + row.db_set("rate_of_depreciation", new_rate_of_depreciation) + new_schedule.rate_of_depreciation = new_rate_of_depreciation - new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc) - - if asset_doc.flags.decrease_in_asset_value_due_to_value_adjustment and not value_after_depreciation: - value_after_depreciation = row.value_after_depreciation - difference_amount - - if asset_doc.flags.increase_in_asset_value_due_to_repair and row.depreciation_method in ( - "Written Down Value", - "Double Declining Balance", - ): - new_rate_of_depreciation = flt( - asset_doc.get_depreciation_rate(row), row.precision("rate_of_depreciation") - ) - row.rate_of_depreciation = new_rate_of_depreciation - new_asset_depr_schedule_doc.rate_of_depreciation = new_rate_of_depreciation - - new_asset_depr_schedule_doc.make_depr_schedule( - asset_doc, row, date_of_disposal, value_after_depreciation=value_after_depreciation - ) - new_asset_depr_schedule_doc.set_accumulated_depreciation( - asset_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, + disposal_date=None, date_of_return=None, update_asset_finance_book_row=False, new_depr_schedule=None, ): - current_asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset_doc.name, "Active", row.finance_book) + current_schedule = get_asset_depr_schedule_doc(asset_doc.name, "Active", row.finance_book) - if not current_asset_depr_schedule_doc: + if not current_schedule: frappe.throw( _("Asset Depreciation Schedule not found for Asset {0} and Finance Book {1}").format( get_link_to_form("Asset", asset_doc.name), row.finance_book ) ) - temp_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc) + temp_asset_depr_schedule_doc = frappe.copy_doc(current_schedule) if new_depr_schedule: temp_asset_depr_schedule_doc.depreciation_schedule = [] @@ -816,10 +253,10 @@ def get_temp_asset_depr_schedule_doc( }, ) - temp_asset_depr_schedule_doc.prepare_draft_asset_depr_schedule_data( + temp_asset_depr_schedule_doc.create_depreciation_schedule( asset_doc, row, - date_of_disposal, + disposal_date, date_of_return, update_asset_finance_book_row, ) @@ -838,7 +275,7 @@ def get_depr_schedule(asset_name, status, finance_book=None): @frappe.whitelist() -def get_asset_depr_schedule_doc(asset_name, status, finance_book=None): +def get_asset_depr_schedule_doc(asset_name, status=None, finance_book=None): asset_depr_schedule = get_asset_depr_schedule_name(asset_name, status, finance_book) if not asset_depr_schedule: @@ -849,16 +286,17 @@ def get_asset_depr_schedule_doc(asset_name, status, finance_book=None): return asset_depr_schedule_doc -def get_asset_depr_schedule_name(asset_name, status, finance_book=None): - if isinstance(status, str): - status = [status] - +def get_asset_depr_schedule_name(asset_name, status=None, finance_book=None): filters = [ ["asset", "=", asset_name], - ["status", "in", status], ["docstatus", "<", 2], ] + if status: + if isinstance(status, str): + status = [status] + filters.append(["status", "in", status]) + if finance_book: filters.append(["finance_book", "=", finance_book]) else: diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/deppreciation_schedule_controller.py b/erpnext/assets/doctype/asset_depreciation_schedule/deppreciation_schedule_controller.py new file mode 100644 index 00000000000..0f72f80e907 --- /dev/null +++ b/erpnext/assets/doctype/asset_depreciation_schedule/deppreciation_schedule_controller.py @@ -0,0 +1,449 @@ +import frappe +from frappe import _ +from frappe.utils import ( + add_days, + add_months, + add_years, + cint, + date_diff, + flt, + get_last_day, + getdate, + is_last_day_of_the_month, + month_diff, + nowdate, +) + +import erpnext +from erpnext.accounts.utils import get_fiscal_year +from erpnext.assets.doctype.asset_depreciation_schedule.depreciation_methods import ( + StraightLineMethod, + WDVMethod, +) + + +class DepreciationScheduleController(StraightLineMethod, WDVMethod): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def create_depreciation_schedule(self, fb_row=None, disposal_date=None): + self.disposal_date = disposal_date + self.asset_doc = frappe.get_doc("Asset", self.asset) + + self.get_finance_book_row(fb_row) + self.fetch_asset_details() + self.clear() + self.create() + self.set_accumulated_depreciation() + + def clear(self): + self.first_non_depreciated_row_idx = 0 + num_of_depreciations_completed = 0 + depr_schedule = [] + + self.schedules_before_clearing = self.get("depreciation_schedule") + + for schedule in self.get("depreciation_schedule"): + if schedule.journal_entry: + num_of_depreciations_completed += 1 + depr_schedule.append(schedule) + else: + self.first_non_depreciated_row_idx = num_of_depreciations_completed + break + + self.depreciation_schedule = depr_schedule + + def create(self): + self.initialize_variables() + for row_idx in range(self.first_non_depreciated_row_idx, self.final_number_of_depreciations): + # If depreciation is already completed (for double declining balance) + if self.skip_row: + continue + + if self.has_fiscal_year_changed(row_idx): + self.yearly_opening_wdv = self.pending_depreciation_amount + + self.get_prev_depreciation_amount(row_idx) + + self.schedule_date = self.get_next_schedule_date(row_idx) + + self.depreciation_amount = self.get_depreciation_amount(row_idx) + print(row_idx, self.schedule_date, self.depreciation_amount) + + # if asset is being sold or scrapped + if self.disposal_date and getdate(self.schedule_date) >= getdate(self.disposal_date): + self.set_depreciation_amount_for_disposal(row_idx) + break + + if row_idx == 0: + self.set_depreciation_amount_for_first_row(row_idx) + elif ( + self.has_pro_rata and row_idx == cint(self.final_number_of_depreciations) - 1 + ): # for the last row + self.set_depreciation_amount_for_last_row(row_idx) + + self.depreciation_amount = flt( + self.depreciation_amount, self.asset_doc.precision("gross_purchase_amount") + ) + if not self.depreciation_amount: + break + + self.pending_depreciation_amount = flt( + self.pending_depreciation_amount - self.depreciation_amount, + self.asset_doc.precision("gross_purchase_amount"), + ) + + self.adjust_depr_amount_for_salvage_value(row_idx) + + if flt(self.depreciation_amount, self.asset_doc.precision("gross_purchase_amount")) > 0: + self.add_depr_schedule_row(row_idx) + + def initialize_variables(self): + self.pending_depreciation_amount = self.fb_row.value_after_depreciation + self.should_get_last_day = is_last_day_of_the_month(self.fb_row.depreciation_start_date) + self.skip_row = False + self.depreciation_amount = 0 + self.prev_per_day_depr = True + self.current_fiscal_year_end_date = None + self.yearly_opening_wdv = self.pending_depreciation_amount + self.get_number_of_pending_months() + self.get_final_number_of_depreciations() + self.is_wdv_or_dd_non_yearly_pro_rata() + self.get_total_pending_days_or_years() + + def get_final_number_of_depreciations(self): + self.final_number_of_depreciations = cint(self.fb_row.total_number_of_depreciations) - cint( + self.opening_number_of_booked_depreciations + ) + + self._check_is_pro_rata() + if self.has_pro_rata: + self.final_number_of_depreciations += 1 + + self.set_final_number_of_depreciations_considering_increase_in_asset_life() + + def set_final_number_of_depreciations_considering_increase_in_asset_life(self): + # final schedule date after increasing asset life + self.final_schedule_date = add_months( + self.asset_doc.available_for_use_date, + (self.fb_row.total_number_of_depreciations * cint(self.fb_row.frequency_of_depreciation)) + + cint(self.fb_row.increase_in_asset_life), + ) + + number_of_pending_depreciations = cint(self.fb_row.total_number_of_depreciations) - cint( + self.asset_doc.opening_number_of_booked_depreciations + ) + schedule_date = add_months( + self.fb_row.depreciation_start_date, + number_of_pending_depreciations * cint(self.fb_row.frequency_of_depreciation), + ) + + if self.final_schedule_date > getdate(schedule_date): + months = month_diff(self.final_schedule_date, schedule_date) + self.final_number_of_depreciations += months // cint(self.fb_row.frequency_of_depreciation) + 1 + + def is_wdv_or_dd_non_yearly_pro_rata(self): + if ( + self.fb_row.depreciation_method in ("Written Down Value", "Double Declining Balance") + and cint(self.fb_row.frequency_of_depreciation) != 12 + ): + self._check_is_pro_rata() + + def _check_is_pro_rata(self): + self.has_pro_rata = False + + # if not existing asset, from_date = available_for_use_date + # otherwise, if opening_number_of_booked_depreciations = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12 + # from_date = 01/01/2022 + if self.fb_row.depreciation_method in ("Straight Line", "Manual"): + prev_depreciation_start_date = get_last_day( + add_months( + self.fb_row.depreciation_start_date, + (self.fb_row.frequency_of_depreciation * -1) + * self.asset_doc.opening_number_of_booked_depreciations, + ) + ) + from_date = self.asset_doc.available_for_use_date + days = date_diff(prev_depreciation_start_date, from_date) + 1 + total_days = self.get_total_days(prev_depreciation_start_date) + else: + from_date = self._get_modified_available_for_use_date_for_existing_assets() + days = date_diff(self.fb_row.depreciation_start_date, from_date) + 1 + total_days = self.get_total_days(self.fb_row.depreciation_start_date) + + if days <= 0: + frappe.throw( + _( + """Error: This asset already has {0} depreciation periods booked. + The `depreciation start` date must be at least {1} periods after the `available for use` date. + Please correct the dates accordingly.""" + ).format( + self.asset_doc.opening_number_of_booked_depreciations, + self.asset_doc.opening_number_of_booked_depreciations, + ) + ) + if days < total_days: + self.has_pro_rata = True + self.has_wdv_or_dd_non_yearly_pro_rata = True + + def _get_modified_available_for_use_date_for_existing_assets(self): + """ + if Asset has opening booked depreciations = 3, + frequency of depreciation = 3, + available for use date = 17-07-2023, + depreciation start date = 30-06-2024 + then from date should be 01-04-2024 + """ + if self.asset_doc.opening_number_of_booked_depreciations > 0: + from_date = add_months( + self.asset_doc.available_for_use_date, + ( + self.asset_doc.opening_number_of_booked_depreciations + * self.fb_row.frequency_of_depreciation + ) + - 1, + ) + if is_last_day_of_the_month(self.fb_row.depreciation_start_date): + return add_days(get_last_day(from_date), 1) + + # get from date when depreciation start date is not last day of the month + months_difference = month_diff(self.fb_row.depreciation_start_date, from_date) - 1 + return add_days(add_months(self.fb_row.depreciation_start_date, -1 * months_difference), 1) + else: + return self.asset_doc.available_for_use_date + + def get_total_days(self, date): + period_start_date = add_months(date, cint(self.fb_row.frequency_of_depreciation) * -1) + if is_last_day_of_the_month(date): + period_start_date = get_last_day(period_start_date) + return date_diff(date, period_start_date) + + def _get_pro_rata_amt(self, from_date, to_date, original_schedule_date=None): + days = date_diff(to_date, from_date) + 1 + months = month_diff(to_date, from_date) + total_days = self.get_total_days(original_schedule_date or to_date) + return (self.depreciation_amount * flt(days)) / flt(total_days), days, months + + def get_number_of_pending_months(self): + total_months = cint(self.fb_row.total_number_of_depreciations) * cint( + self.fb_row.frequency_of_depreciation + ) + cint(self.fb_row.increase_in_asset_life) + depr_booked_for_months = 0 + last_depr_date = self.get_last_booked_depreciation_date() + if last_depr_date: + depr_booked_for_months = date_diff(last_depr_date, self.asset_doc.available_for_use_date) / ( + 365 / 12 + ) + + self.pending_months = total_months - depr_booked_for_months + + def get_last_booked_depreciation_date(self): + last_depr_date = None + if self.first_non_depreciated_row_idx > 0: + last_depr_date = self.depreciation_schedule[self.first_non_depreciated_row_idx - 1].schedule_date + elif self.asset_doc.opening_number_of_booked_depreciations > 0: + last_depr_date = add_months( + self.fb_row.depreciation_start_date, -1 * self.fb_row.frequency_of_depreciation + ) + + return last_depr_date + + def get_total_pending_days_or_years(self): + if cint(frappe.db.get_single_value("Accounts Settings", "calculate_depr_using_total_days")): + last_depr_date = self.get_last_booked_depreciation_date() + self.total_pending_days = date_diff(self.final_schedule_date, last_depr_date) + 1 + else: + self.total_pending_years = self.pending_months / 12 + + def has_fiscal_year_changed(self, row_idx): + self.fiscal_year_changed = False + + schedule_date = get_last_day( + add_months( + self.fb_row.depreciation_start_date, row_idx * cint(self.fb_row.frequency_of_depreciation) + ) + ) + + if not self.current_fiscal_year_end_date: + self.current_fiscal_year_end_date = get_fiscal_year(self.fb_row.depreciation_start_date)[2] + self.fiscal_year_changed = True + elif getdate(schedule_date) > getdate(self.current_fiscal_year_end_date): + self.current_fiscal_year_end_date = add_years(self.current_fiscal_year_end_date, 1) + self.fiscal_year_changed = True + + def get_prev_depreciation_amount(self, row_idx): + self.prev_depreciation_amount = 0 + if row_idx > 0 and len(self.get("depreciation_schedule")) > row_idx - 1: + self.prev_depreciation_amount = self.get("depreciation_schedule")[row_idx - 1].depreciation_amount + + def get_next_schedule_date(self, row_idx): + schedule_date = add_months( + self.fb_row.depreciation_start_date, row_idx * cint(self.fb_row.frequency_of_depreciation) + ) + if self.should_get_last_day: + schedule_date = get_last_day(schedule_date) + + return schedule_date + + def set_depreciation_amount_for_disposal(self, row_idx): + if self.depreciation_schedule: # if there are already booked depreciations + from_date = add_days(self.depreciation_schedule[-1].schedule_date, 1) + else: + from_date = self._get_modified_available_for_use_date_for_existing_assets() + if is_last_day_of_the_month(getdate(self.asset_doc.available_for_use_date)): + from_date = get_last_day(from_date) + + self.depreciation_amount, days, months = self._get_pro_rata_amt( + from_date, + self.disposal_date, + original_schedule_date=self.schedule_date, + ) + + self.depreciation_amount = flt( + self.depreciation_amount, self.asset_doc.precision("gross_purchase_amount") + ) + if self.depreciation_amount > 0: + self.schedule_date = self.disposal_date + self.add_depr_schedule_row(row_idx) + + def set_depreciation_amount_for_first_row(self, row_idx): + """ + For the first row, if available for use date is mid of the month, then pro rata amount is needed + """ + pro_rata_amount_applicable = False + if ( + self.has_pro_rata + and not self.opening_accumulated_depreciation + and not self.flags.wdv_it_act_applied + ): # if not existing asset + from_date = self.asset_doc.available_for_use_date + pro_rata_amount_applicable = True + elif self.has_pro_rata and self.opening_accumulated_depreciation: # if existing asset + from_date = self._get_modified_available_for_use_date_for_existing_assets() + pro_rata_amount_applicable = True + + if pro_rata_amount_applicable: + self.depreciation_amount, days, months = self._get_pro_rata_amt( + from_date, + self.fb_row.depreciation_start_date, + ) + + self.validate_depreciation_amount_for_low_value_assets() + + def set_depreciation_amount_for_last_row(self, row_idx): + if not self.fb_row.increase_in_asset_life: + self.final_schedule_date = add_months( + self.asset_doc.available_for_use_date, + (row_idx + self.opening_number_of_booked_depreciations) + * cint(self.fb_row.frequency_of_depreciation), + ) + if is_last_day_of_the_month(getdate(self.asset_doc.available_for_use_date)): + self.final_schedule_date = get_last_day(self.final_schedule_date) + + if self.opening_accumulated_depreciation: + self.depreciation_amount, days, months = self._get_pro_rata_amt( + self.schedule_date, + self.final_schedule_date, + ) + else: + if not self.fb_row.increase_in_asset_life: + self.depreciation_amount -= self.get("depreciation_schedule")[0].depreciation_amount + days = date_diff(self.final_schedule_date, self.schedule_date) + 1 + + self.schedule_date = add_days(self.schedule_date, days - 1) + + def adjust_depr_amount_for_salvage_value(self, row_idx): + """ + Adjust depreciation amount in the last period based on the expected value after useful life + """ + if ( + row_idx == cint(self.final_number_of_depreciations) - 1 + and flt(self.pending_depreciation_amount) != flt(self.fb_row.expected_value_after_useful_life) + ) or flt(self.pending_depreciation_amount) < flt(self.fb_row.expected_value_after_useful_life): + self.depreciation_amount += flt(self.pending_depreciation_amount) - flt( + self.fb_row.expected_value_after_useful_life + ) + self.depreciation_amount = flt( + self.depreciation_amount, self.precision("value_after_depreciation") + ) + self.skip_row = True + + def validate_depreciation_amount_for_low_value_assets(self): + """ + If gross purchase amount is too low, then depreciation amount + can come zero sometimes based on the frequency and number of depreciations. + """ + if flt(self.depreciation_amount, self.asset_doc.precision("gross_purchase_amount")) <= 0: + frappe.throw( + _("Gross Purchase Amount {0} cannot be depreciated over {1} cycles.").format( + frappe.bold(self.asset_doc.gross_purchase_amount), + frappe.bold(self.fb_row.total_number_of_depreciations), + ) + ) + + def add_depr_schedule_row(self, row_idx): + shift = None + if self.shift_based: + shift = ( + self.schedules_before_clearing[row_idx].shift + if (self.schedules_before_clearing and len(self.schedules_before_clearing) > row_idx) + else frappe.get_cached_value("Asset Shift Factor", {"default": 1}, "shift_name") + ) + + self.append( + "depreciation_schedule", + { + "schedule_date": self.schedule_date, + "depreciation_amount": self.depreciation_amount, + "shift": shift, + }, + ) + + def set_accumulated_depreciation(self): + accumulated_depreciation = flt(self.opening_accumulated_depreciation) + for d in self.get("depreciation_schedule"): + if d.journal_entry: + accumulated_depreciation = d.accumulated_depreciation_amount + continue + + accumulated_depreciation += d.depreciation_amount + d.accumulated_depreciation_amount = flt( + accumulated_depreciation, d.precision("accumulated_depreciation_amount") + ) + + def get_depreciation_amount(self, row_idx): + if self.fb_row.depreciation_method in ("Straight Line", "Manual"): + return self.get_straight_line_depr_amount(row_idx) + else: + return self.get_wdv_or_dd_depr_amount(row_idx) + + def _get_total_days(self, depreciation_start_date, row_idx): + from_date = add_months(depreciation_start_date, (row_idx - 1) * self.frequency_of_depreciation) + to_date = add_months(from_date, self.frequency_of_depreciation) + if is_last_day_of_the_month(depreciation_start_date): + to_date = get_last_day(to_date) + from_date = add_days(get_last_day(from_date), 1) + return from_date, date_diff(to_date, from_date) + 1 + + def get_total_days_in_current_depr_year(self): + fy_start_date, fy_end_date = self.get_fiscal_year(self.schedule_date) + return date_diff(fy_end_date, fy_start_date) + 1 + + def get_fiscal_year(self, date): + fy = get_fiscal_year(date, as_dict=True, raise_on_missing=False) + if fy: + fy_start_date = fy.year_start_date + fy_end_date = fy.year_end_date + else: + current_fy = get_fiscal_year(nowdate(), as_dict=True) + # get fiscal year start date of the year in which the schedule date falls + months = month_diff(date, current_fy.year_start_date) + if months % 12: + years = months // 12 + else: + years = months // 12 - 1 + + fy_start_date = add_years(current_fy.year_start_date, years) + fy_end_date = add_days(add_years(fy_start_date, 1), -1) + + return fy_start_date, fy_end_date diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/depreciation_methods.py b/erpnext/assets/doctype/asset_depreciation_schedule/depreciation_methods.py new file mode 100644 index 00000000000..9a573ea05e1 --- /dev/null +++ b/erpnext/assets/doctype/asset_depreciation_schedule/depreciation_methods.py @@ -0,0 +1,119 @@ +import frappe +from frappe.model.document import Document +from frappe.utils import ( + add_days, + add_years, + cint, + date_diff, + flt, + month_diff, + nowdate, +) + +import erpnext +from erpnext.accounts.utils import get_fiscal_year + +# from erpnext.assets.doctype.asset_depreciation_schedule.deppreciation_schedule_controller import ( +# _get_total_days, +# ) + + +class StraightLineMethod(Document): + def get_straight_line_depr_amount(self, row_idx): + self.depreciable_value = flt(self.fb_row.value_after_depreciation) - flt( + self.fb_row.expected_value_after_useful_life + ) + + if self.fb_row.shift_based: + self.get_shift_depr_amount(row_idx) + + if self.fb_row.daily_prorata_based: + return self.get_daily_prorata_based_depr_amount(row_idx) + else: + return self.get_fixed_depr_amount() + + def get_fixed_depr_amount(self): + pending_periods = flt(self.pending_months) / flt(self.fb_row.frequency_of_depreciation) + return self.depreciable_value / pending_periods + + def get_daily_prorata_based_depr_amount(self, row_idx): + daily_depr_amount = self.get_daily_depr_amount() + + from_date, total_depreciable_days = self._get_total_days(self.fb_row.depreciation_start_date, row_idx) + return daily_depr_amount * total_depreciable_days + + def get_daily_depr_amount(self): + if cint(frappe.db.get_single_value("Accounts Settings", "calculate_depr_using_total_days")): + return self.depreciable_value / self.total_pending_days + else: + yearly_depr_amount = self.depreciable_value / self.total_pending_years + total_days_in_current_depr_year = self.get_total_days_in_current_depr_year() + return yearly_depr_amount / total_days_in_current_depr_year + + def get_shift_depr_amount(self, row_idx): + depreciable_value = ( + flt(self.asset_doc.gross_purchase_amount) + - flt(self.asset_doc.opening_accumulated_depreciation) + - flt(self.fb_row.expected_value_after_useful_life) + ) + if self.get("__islocal") and not self.asset_doc.flags.shift_allocation: + pending_depreciations = flt( + self.fb_row.total_number_of_depreciations + - self.asset_doc.opening_number_of_booked_depreciations + ) + return depreciable_value / pending_depreciations + + asset_shift_factors_map = self.get_asset_shift_factors_map() + shift = ( + self.schedules_before_clearing[row_idx].shift + if len(self.schedules_before_clearing) > row_idx + else None + ) + shift_factor = asset_shift_factors_map.get(shift, 0) + + shift_factors_sum = sum( + [flt(asset_shift_factors_map.get(d.shift)) for d in self.schedules_before_clearing] + ) + + return (depreciable_value / shift_factors_sum) * shift_factor + + def get_asset_shift_factors_map(self): + return dict(frappe.db.get_all("Asset Shift Factor", ["shift_name", "shift_factor"], as_list=True)) + + +class WDVMethod(Document): + def get_wdv_or_dd_depr_amount(self, row_idx): + if self.fb_row.daily_prorata_based: + return self.get_daily_prorata_based_wdv_depr_amount(row_idx) + else: + return self.get_wdv_depr_amount() + + def get_wdv_depr_amount(self): + if self.is_fiscal_year_changed(): + yearly_amount = ( + flt(self.pending_depreciation_amount) * flt(self.fb_row.rate_of_depreciation) / 100 + ) + return (yearly_amount * self.fb_row.frequency_of_depreciation) / 12 + else: + return self.prev_depreciation_amount + + def is_fiscal_year_changed(self): + fy_start_date, fy_end_date = self.get_fiscal_year(self.schedule_date) + if fy_start_date != self.get("prev_fy_start_date"): + self.prev_fy_start_date = fy_start_date + return True + + def get_daily_prorata_based_wdv_depr_amount(self, row_idx): + daily_depr_amount = self.get_daily_wdv_depr_amount() + + from_date, total_depreciable_days = self._get_total_days(self.fb_row.depreciation_start_date, row_idx) + return daily_depr_amount * total_depreciable_days + + def get_daily_wdv_depr_amount(self): + if self.is_fiscal_year_changed(): + self.yearly_wdv_depr_amount = ( + self.pending_depreciation_amount * self.fb_row.rate_of_depreciation / 100 + ) + + total_days_in_current_depr_year = self.get_total_days_in_current_depr_year() + return self.yearly_wdv_depr_amount / total_days_in_current_depr_year diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/utils.py b/erpnext/assets/doctype/asset_depreciation_schedule/utils.py deleted file mode 100644 index c13ad5ebdcb..00000000000 --- a/erpnext/assets/doctype/asset_depreciation_schedule/utils.py +++ /dev/null @@ -1,333 +0,0 @@ -import frappe -from frappe.utils import ( - add_days, - add_months, - add_years, - cint, - cstr, - date_diff, - flt, - get_last_day, - is_last_day_of_the_month, -) - -import erpnext - - -def get_depreciation_amount( - asset_depr_schedule, - asset, - value_after_depreciation, - yearly_opening_wdv, - fb_row, - schedule_idx=0, - prev_depreciation_amount=0, - has_wdv_or_dd_non_yearly_pro_rata=False, - number_of_pending_depreciations=0, - prev_per_day_depr=0, -): - if fb_row.depreciation_method in ("Straight Line", "Manual"): - return get_straight_line_or_manual_depr_amount( - asset_depr_schedule, - asset, - fb_row, - schedule_idx, - value_after_depreciation, - number_of_pending_depreciations, - ), None - else: - return get_wdv_or_dd_depr_amount( - asset, - fb_row, - value_after_depreciation, - yearly_opening_wdv, - schedule_idx, - prev_depreciation_amount, - has_wdv_or_dd_non_yearly_pro_rata, - asset_depr_schedule, - prev_per_day_depr, - ) - - -def get_straight_line_or_manual_depr_amount( - asset_depr_schedule, - asset, - fb_row, - schedule_idx, - value_after_depreciation, - number_of_pending_depreciations, -): - if fb_row.shift_based: - return get_shift_depr_amount(asset_depr_schedule, asset, fb_row, schedule_idx) - - if fb_row.daily_prorata_based: - amount = flt(asset.gross_purchase_amount) - flt(fb_row.expected_value_after_useful_life) - return get_daily_prorata_based_straight_line_depr( - asset, fb_row, schedule_idx, number_of_pending_depreciations, amount - ) - else: - return (flt(fb_row.value_after_depreciation) - flt(fb_row.expected_value_after_useful_life)) / ( - flt(number_of_pending_depreciations) / flt(fb_row.frequency_of_depreciation) - ) - - -def get_daily_prorata_based_straight_line_depr( - asset, fb_row, schedule_idx, number_of_pending_depreciations, amount -): - daily_depr_amount = get_daily_depr_amount(asset, fb_row, schedule_idx, amount) - - from_date, total_depreciable_days = _get_total_days( - fb_row.depreciation_start_date, schedule_idx, fb_row.frequency_of_depreciation - ) - return daily_depr_amount * total_depreciable_days - - -def get_daily_depr_amount(asset, fb_row, schedule_idx, amount): - if cint(frappe.db.get_single_value("Accounts Settings", "calculate_depr_using_total_days")): - total_days = ( - date_diff( - get_last_day( - add_months( - fb_row.depreciation_start_date, - flt( - fb_row.total_number_of_depreciations - - asset.opening_number_of_booked_depreciations - - 1 - ) - * fb_row.frequency_of_depreciation, - ) - ), - add_days( - get_last_day( - add_months( - fb_row.depreciation_start_date, - ( - fb_row.frequency_of_depreciation - * (asset.opening_number_of_booked_depreciations + 1) - ) - * -1, - ), - ), - 1, - ), - ) - + 1 - ) - - return amount / total_days - else: - total_years = ( - flt( - (fb_row.total_number_of_depreciations - fb_row.total_number_of_booked_depreciations) - * fb_row.frequency_of_depreciation - ) - / 12 - ) - - every_year_depr = amount / total_years - - depr_period_start_date = add_days( - get_last_day(add_months(fb_row.depreciation_start_date, fb_row.frequency_of_depreciation * -1)), 1 - ) - - year_start_date = add_years( - depr_period_start_date, ((fb_row.frequency_of_depreciation * schedule_idx) // 12) - ) - year_end_date = add_days(add_years(year_start_date, 1), -1) - - return every_year_depr / (date_diff(year_end_date, year_start_date) + 1) - - -def get_shift_depr_amount(asset_depr_schedule, asset, fb_row, schedule_idx): - if asset_depr_schedule.get("__islocal") and not asset.flags.shift_allocation: - return ( - flt(asset.gross_purchase_amount) - - flt(asset.opening_accumulated_depreciation) - - flt(fb_row.expected_value_after_useful_life) - ) / flt(fb_row.total_number_of_depreciations - asset.opening_number_of_booked_depreciations) - - asset_shift_factors_map = get_asset_shift_factors_map() - shift = ( - asset_depr_schedule.schedules_before_clearing[schedule_idx].shift - if len(asset_depr_schedule.schedules_before_clearing) > schedule_idx - else None - ) - shift_factor = asset_shift_factors_map.get(shift) if shift else 0 - - shift_factors_sum = sum( - flt(asset_shift_factors_map.get(schedule.shift)) - for schedule in asset_depr_schedule.schedules_before_clearing - ) - - return ( - ( - flt(asset.gross_purchase_amount) - - flt(asset.opening_accumulated_depreciation) - - flt(fb_row.expected_value_after_useful_life) - ) - / flt(shift_factors_sum) - ) * shift_factor - - -def get_asset_shift_factors_map(): - return dict(frappe.db.get_all("Asset Shift Factor", ["shift_name", "shift_factor"], as_list=True)) - - -@erpnext.allow_regional -def get_wdv_or_dd_depr_amount( - asset, - fb_row, - value_after_depreciation, - yearly_opening_wdv, - schedule_idx, - prev_depreciation_amount, - has_wdv_or_dd_non_yearly_pro_rata, - asset_depr_schedule, - prev_per_day_depr, -): - return get_default_wdv_or_dd_depr_amount( - asset, - fb_row, - value_after_depreciation, - schedule_idx, - prev_depreciation_amount, - has_wdv_or_dd_non_yearly_pro_rata, - asset_depr_schedule, - prev_per_day_depr, - ) - - -def get_default_wdv_or_dd_depr_amount( - asset, - fb_row, - value_after_depreciation, - schedule_idx, - prev_depreciation_amount, - has_wdv_or_dd_non_yearly_pro_rata, - asset_depr_schedule, - prev_per_day_depr, -): - if not fb_row.daily_prorata_based or cint(fb_row.frequency_of_depreciation) == 12: - return _get_default_wdv_or_dd_depr_amount( - asset, - fb_row, - value_after_depreciation, - schedule_idx, - prev_depreciation_amount, - has_wdv_or_dd_non_yearly_pro_rata, - asset_depr_schedule, - ), None - else: - return _get_daily_prorata_based_default_wdv_or_dd_depr_amount( - asset, - fb_row, - value_after_depreciation, - schedule_idx, - prev_depreciation_amount, - has_wdv_or_dd_non_yearly_pro_rata, - asset_depr_schedule, - prev_per_day_depr, - ) - - -def _get_default_wdv_or_dd_depr_amount( - asset, - fb_row, - value_after_depreciation, - schedule_idx, - prev_depreciation_amount, - has_wdv_or_dd_non_yearly_pro_rata, - asset_depr_schedule, -): - if cint(fb_row.frequency_of_depreciation) == 12: - return flt(value_after_depreciation) * (flt(fb_row.rate_of_depreciation) / 100) - else: - if has_wdv_or_dd_non_yearly_pro_rata: - if schedule_idx == 0: - return flt(value_after_depreciation) * (flt(fb_row.rate_of_depreciation) / 100) - elif schedule_idx % (12 / cint(fb_row.frequency_of_depreciation)) == 1: - return ( - flt(value_after_depreciation) - * flt(fb_row.frequency_of_depreciation) - * (flt(fb_row.rate_of_depreciation) / 1200) - ) - else: - return prev_depreciation_amount - else: - if schedule_idx % (12 / cint(fb_row.frequency_of_depreciation)) == 0: - return ( - flt(value_after_depreciation) - * flt(fb_row.frequency_of_depreciation) - * (flt(fb_row.rate_of_depreciation) / 1200) - ) - else: - return prev_depreciation_amount - - -def _get_daily_prorata_based_default_wdv_or_dd_depr_amount( - asset, - fb_row, - value_after_depreciation, - schedule_idx, - prev_depreciation_amount, - has_wdv_or_dd_non_yearly_pro_rata, - asset_depr_schedule, - prev_per_day_depr, -): - if has_wdv_or_dd_non_yearly_pro_rata: # If applicable days for ther first month is less than full month - if schedule_idx == 0: - return flt(value_after_depreciation) * (flt(fb_row.rate_of_depreciation) / 100), None - - elif schedule_idx % (12 / cint(fb_row.frequency_of_depreciation)) == 1: # Year changes - return get_monthly_depr_amount(fb_row, schedule_idx, value_after_depreciation) - else: - return get_monthly_depr_amount_based_on_prev_per_day_depr(fb_row, schedule_idx, prev_per_day_depr) - else: - if schedule_idx % (12 / cint(fb_row.frequency_of_depreciation)) == 0: # year changes - return get_monthly_depr_amount(fb_row, schedule_idx, value_after_depreciation) - else: - return get_monthly_depr_amount_based_on_prev_per_day_depr(fb_row, schedule_idx, prev_per_day_depr) - - -def get_monthly_depr_amount(fb_row, schedule_idx, value_after_depreciation): - """ - Returns monthly depreciation amount when year changes - 1. Calculate per day depr based on new year - 2. Calculate monthly amount based on new per day amount - """ - from_date, days_in_month = _get_total_days( - fb_row.depreciation_start_date, schedule_idx, cint(fb_row.frequency_of_depreciation) - ) - per_day_depr = get_per_day_depr(fb_row, value_after_depreciation, from_date) - return (per_day_depr * days_in_month), per_day_depr - - -def get_monthly_depr_amount_based_on_prev_per_day_depr(fb_row, schedule_idx, prev_per_day_depr): - """ " - Returns monthly depreciation amount based on prev per day depr - Calculate per day depr only for the first month - """ - from_date, days_in_month = _get_total_days( - fb_row.depreciation_start_date, schedule_idx, cint(fb_row.frequency_of_depreciation) - ) - return (prev_per_day_depr * days_in_month), prev_per_day_depr - - -def get_per_day_depr( - fb_row, - value_after_depreciation, - from_date, -): - to_date = add_days(add_years(from_date, 1), -1) - total_days = date_diff(to_date, from_date) + 1 - per_day_depr = (flt(value_after_depreciation) * (flt(fb_row.rate_of_depreciation) / 100)) / total_days - return per_day_depr - - -def _get_total_days(depreciation_start_date, schedule_idx, frequency_of_depreciation): - from_date = add_months(depreciation_start_date, (schedule_idx - 1) * frequency_of_depreciation) - to_date = add_months(from_date, frequency_of_depreciation) - if is_last_day_of_the_month(depreciation_start_date): - to_date = get_last_day(to_date) - from_date = add_days(get_last_day(from_date), 1) - return from_date, date_diff(to_date, from_date) + 1 diff --git a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json index be035f7ea7a..eb726a1a1f7 100644 --- a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json +++ b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json @@ -90,7 +90,8 @@ "fieldname": "rate_of_depreciation", "fieldtype": "Percent", "label": "Rate of Depreciation (%)", - "mandatory_depends_on": "eval:doc.depreciation_method == 'Written Down Value'" + "mandatory_depends_on": "eval:doc.depreciation_method == 'Written Down Value'", + "no_copy": 1 }, { "fieldname": "salvage_value_percentage", @@ -116,6 +117,7 @@ "fieldname": "total_number_of_booked_depreciations", "fieldtype": "Int", "label": "Total Number of Booked Depreciations ", + "no_copy": 1, "read_only": 1 }, { @@ -138,7 +140,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2024-12-19 17:50:24.012434", + "modified": "2025-01-06 17:14:51.836803", "modified_by": "Administrator", "module": "Assets", "name": "Asset Finance Book", diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 54a067063b2..f49de50838a 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -157,6 +157,7 @@ "options": "Asset Repair Consumed Item" }, { + "fetch_from": "company.cost_center", "fieldname": "cost_center", "fieldtype": "Link", "label": "Cost Center", @@ -258,7 +259,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2024-12-23 18:08:35.159964", + "modified": "2024-12-27 18:11:40.548727", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 8b541163179..27569eb6cce 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -12,7 +12,7 @@ from erpnext.assets.doctype.asset.asset import get_asset_account from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( get_depr_schedule, - make_new_active_asset_depr_schedules_and_cancel_current_ones, + reschedule_depreciation, ) from erpnext.controllers.accounts_controller import AccountsController @@ -144,30 +144,27 @@ class AssetRepair(AccountsController): self.total_repair_cost = flt(self.repair_cost) + flt(self.consumed_items_cost) def on_submit(self): - self.asset_doc.flags.increase_in_asset_value_due_to_repair = False self.decrease_stock_quantity() if self.get("capitalize_repair_cost"): self.update_asset_value() - self.make_gl_entries() self.set_increase_in_asset_life() depreciation_note = self.get_depreciation_note() - make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, depreciation_note) + reschedule_depreciation(self.asset_doc, depreciation_note) self.add_asset_activity() + self.make_gl_entries() + def on_cancel(self): self.asset_doc = frappe.get_doc("Asset", self.asset) - if self.get("capitalize_repair_cost"): - self.asset_doc.flags.increase_in_asset_value_due_to_repair = True - self.update_asset_value() self.make_gl_entries(cancel=True) self.set_increase_in_asset_life() depreciation_note = self.get_depreciation_note() - make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, depreciation_note) + reschedule_depreciation(self.asset_doc, depreciation_note) self.add_asset_activity() def after_delete(self): @@ -358,9 +355,10 @@ class AssetRepair(AccountsController): def set_increase_in_asset_life(self): if self.asset_doc.calculate_depreciation and cint(self.increase_in_asset_life) > 0: for row in self.asset_doc.finance_books: - row.increase_in_asset_life = row.increase_in_asset_life + ( + row.increase_in_asset_life = cint(row.increase_in_asset_life) + ( cint(self.increase_in_asset_life) * (1 if self.docstatus == 1 else -1) ) + row.db_update() def get_depreciation_note(self): return _("This schedule was created when Asset {0} was repaired through Asset Repair {1}.").format( diff --git a/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.py b/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.py index 0c0da3bd8cd..4c5253b338d 100644 --- a/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.py +++ b/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.py @@ -18,7 +18,9 @@ from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_sched get_asset_depr_schedule_doc, get_temp_asset_depr_schedule_doc, ) -from erpnext.assets.doctype.asset_depreciation_schedule.utils import get_asset_shift_factors_map +from erpnext.erpnext.assets.doctype.asset_depreciation_schedule.deppreciation_schedule_controller import ( + get_asset_shift_factors_map, +) class AssetShiftAllocation(Document): 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 07817d8e6ad..f27ffb0d945 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py @@ -14,7 +14,7 @@ from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciatio from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( - make_new_active_asset_depr_schedules_and_cancel_current_ones, + reschedule_depreciation, ) @@ -64,7 +64,7 @@ class AssetValueAdjustment(Document): self.current_asset_value = get_asset_value_after_depreciation(self.asset, self.finance_book) def on_submit(self): - self.make_depreciation_entry() + self.make_asset_revaluation_entry() self.update_asset() add_asset_activity( self.asset, @@ -83,7 +83,7 @@ class AssetValueAdjustment(Document): ), ) - def make_depreciation_entry(self): + def make_asset_revaluation_entry(self): asset = frappe.get_doc("Asset", self.asset) ( fixed_asset_account, @@ -170,7 +170,7 @@ class AssetValueAdjustment(Document): def update_asset(self): asset = self.update_asset_value_after_depreciation() note = self.get_adjustment_note() - make_new_active_asset_depr_schedules_and_cancel_current_ones(asset, note) + reschedule_depreciation(asset, note) def update_asset_value_after_depreciation(self): difference_amount = self.difference_amount if self.docstatus == 1 else -1 * self.difference_amount diff --git a/erpnext/patches.txt b/erpnext/patches.txt index ec3ae09117b..0df74cf36d5 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -411,3 +411,4 @@ erpnext.patches.v15_0.update_payment_schedule_fields_in_invoices erpnext.patches.v15_0.rename_group_by_to_categorize_by execute:frappe.db.set_single_value("Accounts Settings", "receivable_payable_fetch_method", "Buffered Cursor") erpnext.patches.v14_0.set_update_price_list_based_on +erpnext.patches.v15_0.update_journal_entry_type 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 index d4350d8f9a1..ba923b4e701 100644 --- a/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py +++ b/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py @@ -22,7 +22,7 @@ def execute(): asset_depr_schedule_doc.name, {"docstatus": 1, "status": "Active"}, ) - + update_depreciation_schedules(depreciation_schedules, asset_depr_schedule_doc.name) diff --git a/erpnext/patches/v15_0/update_journal_entry_type.py b/erpnext/patches/v15_0/update_journal_entry_type.py new file mode 100644 index 00000000000..32a499d01af --- /dev/null +++ b/erpnext/patches/v15_0/update_journal_entry_type.py @@ -0,0 +1,18 @@ +import frappe + + +def execute(): + custom_je_type = frappe.db.get_value( + "Property Setter", + {"doc_type": "Journal Entry", "field_name": "voucher_type", "property": "options"}, + ["name", "value"], + ) + if custom_je_type: + custom_je_type.value += "\nAsset Disposal" + frappe.db.set_value("Property Setter", custom_je_type.name, "value", custom_je_type.value) + + scrapped_journal_entries = frappe.get_all( + "Asset", filters={"journal_entry_for_scrap": ["is", "not set"]}, fields=["name"] + ) + for je in scrapped_journal_entries: + frappe.db.set_value("Journal Entry", je.name, "voucher_type", "Asset Disposal")