diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 153f5c537a..f414930d72 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -108,6 +108,10 @@ frappe.ui.form.on('Asset', { frm.trigger("create_asset_repair"); }, __("Manage")); + frm.add_custom_button(__("Split Asset"), function() { + frm.trigger("split_asset"); + }, __("Manage")); + if (frm.doc.status != 'Fully Depreciated') { frm.add_custom_button(__("Adjust Asset Value"), function() { frm.trigger("create_asset_value_adjustment"); @@ -322,6 +326,43 @@ frappe.ui.form.on('Asset', { }); }, + split_asset: function(frm) { + const title = __('Split Asset'); + + const fields = [ + { + fieldname: 'split_qty', + fieldtype: 'Int', + label: __('Split Qty'), + reqd: 1 + } + ]; + + let dialog = new frappe.ui.Dialog({ + title: title, + fields: fields + }); + + dialog.set_primary_action(__('Split'), function() { + const dialog_data = dialog.get_values(); + frappe.call({ + args: { + "asset_name": frm.doc.name, + "split_qty": cint(dialog_data.split_qty) + }, + method: "erpnext.assets.doctype.asset.asset.split_asset", + callback: function(r) { + let doclist = frappe.model.sync(r.message); + frappe.set_route("Form", doclist[0].doctype, doclist[0].name); + } + }); + + dialog.hide(); + }); + + dialog.show(); + }, + create_asset_value_adjustment: function(frm) { frappe.call({ args: { diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index 0a7c041396..2a8e0ed369 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -3,7 +3,7 @@ "allow_import": 1, "allow_rename": 1, "autoname": "naming_series:", - "creation": "2016-03-01 17:01:27.920130", + "creation": "2022-01-18 02:26:55.975005", "doctype": "DocType", "document_type": "Document", "engine": "InnoDB", @@ -23,6 +23,7 @@ "asset_name", "asset_category", "location", + "split_from", "custodian", "department", "disposal_date", @@ -482,6 +483,13 @@ "fieldtype": "Section Break", "label": "Finance Books" }, + { + "fieldname": "split_from", + "fieldtype": "Link", + "label": "Split From", + "options": "Asset", + "read_only": 1 + }, { "fieldname": "asset_quantity", "fieldtype": "Int", @@ -509,7 +517,7 @@ "link_fieldname": "asset" } ], - "modified": "2022-01-18 12:57:36.741192", + "modified": "2022-01-20 12:57:36.741192", "modified_by": "Administrator", "module": "Assets", "name": "Asset", diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index ac64a95a53..6e87426ccb 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -38,7 +38,8 @@ class Asset(AccountsController): self.validate_item() self.validate_cost_center() self.set_missing_values() - self.prepare_depreciation_data() + if not self.split_from: + self.prepare_depreciation_data() self.validate_gross_and_purchase_amount() if self.get("schedules"): self.validate_expected_value_after_useful_life() @@ -202,143 +203,143 @@ class Asset(AccountsController): start = self.clear_depreciation_schedule() for finance_book in self.get('finance_books'): - self.validate_asset_finance_books(finance_book) + self._make_depreciation_schedule(finance_book, start, date_of_sale) - # value_after_depreciation - current Asset value - if self.docstatus == 1 and finance_book.value_after_depreciation: - value_after_depreciation = flt(finance_book.value_after_depreciation) - else: - value_after_depreciation = (flt(self.gross_purchase_amount) - - flt(self.opening_accumulated_depreciation)) + def _make_depreciation_schedule(self, finance_book, start, date_of_sale): + self.validate_asset_finance_books(finance_book) - finance_book.value_after_depreciation = value_after_depreciation + value_after_depreciation = self._get_value_after_depreciation(finance_book) + finance_book.value_after_depreciation = value_after_depreciation - number_of_pending_depreciations = cint(finance_book.total_number_of_depreciations) - \ - cint(self.number_of_depreciations_booked) + number_of_pending_depreciations = cint(finance_book.total_number_of_depreciations) - \ + cint(self.number_of_depreciations_booked) - has_pro_rata = self.check_is_pro_rata(finance_book) + has_pro_rata = self.check_is_pro_rata(finance_book) + if has_pro_rata: + number_of_pending_depreciations += 1 - if has_pro_rata: - number_of_pending_depreciations += 1 + skip_row = False - skip_row = False + for n in range(start[finance_book.idx-1], number_of_pending_depreciations): + # If depreciation is already completed (for double declining balance) + if skip_row: continue - for n in range(start[finance_book.idx-1], number_of_pending_depreciations): - # If depreciation is already completed (for double declining balance) - if skip_row: continue + depreciation_amount = get_depreciation_amount(self, value_after_depreciation, finance_book) - depreciation_amount = get_depreciation_amount(self, value_after_depreciation, finance_book) + if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1: + schedule_date = add_months(finance_book.depreciation_start_date, + n * cint(finance_book.frequency_of_depreciation)) - if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1: - schedule_date = add_months(finance_book.depreciation_start_date, - n * cint(finance_book.frequency_of_depreciation)) + # schedule date will be a year later from start date + # so monthly schedule date is calculated by removing 11 months from it + monthly_schedule_date = add_months(schedule_date, - finance_book.frequency_of_depreciation + 1) - # schedule date will be a year later from start date - # so monthly schedule date is calculated by removing 11 months from it - monthly_schedule_date = add_months(schedule_date, - finance_book.frequency_of_depreciation + 1) - - # if asset is being sold - if date_of_sale: - from_date = self.get_from_date(finance_book.finance_book) - depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount, - from_date, date_of_sale) - - if depreciation_amount > 0: - self.append("schedules", { - "schedule_date": date_of_sale, - "depreciation_amount": depreciation_amount, - "depreciation_method": finance_book.depreciation_method, - "finance_book": finance_book.finance_book, - "finance_book_id": finance_book.idx - }) - - break - - # For first row - if has_pro_rata and not self.opening_accumulated_depreciation and n==0: - from_date = add_days(self.available_for_use_date, -1) # needed to calc depr amount for available_for_use_date too - depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount, - from_date, finance_book.depreciation_start_date) - - # For first depr schedule date will be the start date - # so monthly schedule date is calculated by removing month difference between use date and start date - monthly_schedule_date = add_months(finance_book.depreciation_start_date, - months + 1) - - # For last row - elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1: - if not self.flags.increase_in_asset_life: - # In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission - self.to_date = add_months(self.available_for_use_date, - (n + self.number_of_depreciations_booked) * cint(finance_book.frequency_of_depreciation)) - - depreciation_amount_without_pro_rata = depreciation_amount - - depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, - depreciation_amount, schedule_date, self.to_date) - - depreciation_amount = self.get_adjusted_depreciation_amount(depreciation_amount_without_pro_rata, - depreciation_amount, finance_book.finance_book) - - monthly_schedule_date = add_months(schedule_date, 1) - schedule_date = add_days(schedule_date, days) - last_schedule_date = schedule_date - - if not depreciation_amount: continue - value_after_depreciation -= flt(depreciation_amount, - self.precision("gross_purchase_amount")) - - # Adjust depreciation amount in the last period based on the expected value after useful life - if finance_book.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1 - and value_after_depreciation != finance_book.expected_value_after_useful_life) - or value_after_depreciation < finance_book.expected_value_after_useful_life): - depreciation_amount += (value_after_depreciation - finance_book.expected_value_after_useful_life) - skip_row = True + # if asset is being sold + if date_of_sale: + from_date = self.get_from_date(finance_book.finance_book) + depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount, + from_date, date_of_sale) if depreciation_amount > 0: - # With monthly depreciation, each depreciation is divided by months remaining until next date - if self.allow_monthly_depreciation: - # month range is 1 to 12 - # In pro rata case, for first and last depreciation, month range would be different - month_range = months \ - if (has_pro_rata and n==0) or (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) \ - else finance_book.frequency_of_depreciation + self._add_depreciation_row(date_of_sale, depreciation_amount, finance_book.depreciation_method, + finance_book.finance_book, finance_book.idx) - for r in range(month_range): - if (has_pro_rata and n == 0): - # For first entry of monthly depr - if r == 0: - days_until_first_depr = date_diff(monthly_schedule_date, self.available_for_use_date) - per_day_amt = depreciation_amount / days - depreciation_amount_for_current_month = per_day_amt * days_until_first_depr - depreciation_amount -= depreciation_amount_for_current_month - date = monthly_schedule_date - amount = depreciation_amount_for_current_month - else: - date = add_months(monthly_schedule_date, r) - amount = depreciation_amount / (month_range - 1) - elif (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) and r == cint(month_range) - 1: - # For last entry of monthly depr - date = last_schedule_date - amount = depreciation_amount / month_range + break + + # For first row + if has_pro_rata and not self.opening_accumulated_depreciation and n==0: + from_date = add_days(self.available_for_use_date, -1) # needed to calc depr amount for available_for_use_date too + depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount, + from_date, finance_book.depreciation_start_date) + + # For first depr schedule date will be the start date + # so monthly schedule date is calculated by removing month difference between use date and start date + monthly_schedule_date = add_months(finance_book.depreciation_start_date, - months + 1) + + # For last row + elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1: + if not self.flags.increase_in_asset_life: + # In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission + self.to_date = add_months(self.available_for_use_date, + (n + self.number_of_depreciations_booked) * cint(finance_book.frequency_of_depreciation)) + + depreciation_amount_without_pro_rata = depreciation_amount + + depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, + depreciation_amount, schedule_date, self.to_date) + + depreciation_amount = self.get_adjusted_depreciation_amount(depreciation_amount_without_pro_rata, + depreciation_amount, finance_book.finance_book) + + monthly_schedule_date = add_months(schedule_date, 1) + schedule_date = add_days(schedule_date, days) + last_schedule_date = schedule_date + + if not depreciation_amount: continue + value_after_depreciation -= flt(depreciation_amount, + self.precision("gross_purchase_amount")) + + # Adjust depreciation amount in the last period based on the expected value after useful life + if finance_book.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1 + and value_after_depreciation != finance_book.expected_value_after_useful_life) + or value_after_depreciation < finance_book.expected_value_after_useful_life): + depreciation_amount += (value_after_depreciation - finance_book.expected_value_after_useful_life) + skip_row = True + + if depreciation_amount > 0: + # With monthly depreciation, each depreciation is divided by months remaining until next date + if self.allow_monthly_depreciation: + # month range is 1 to 12 + # In pro rata case, for first and last depreciation, month range would be different + month_range = months \ + if (has_pro_rata and n==0) or (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) \ + else finance_book.frequency_of_depreciation + + for r in range(month_range): + if (has_pro_rata and n == 0): + # For first entry of monthly depr + if r == 0: + days_until_first_depr = date_diff(monthly_schedule_date, self.available_for_use_date) + per_day_amt = depreciation_amount / days + depreciation_amount_for_current_month = per_day_amt * days_until_first_depr + depreciation_amount -= depreciation_amount_for_current_month + date = monthly_schedule_date + amount = depreciation_amount_for_current_month else: date = add_months(monthly_schedule_date, r) - amount = depreciation_amount / month_range + amount = depreciation_amount / (month_range - 1) + elif (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) and r == cint(month_range) - 1: + # For last entry of monthly depr + date = last_schedule_date + amount = depreciation_amount / month_range + else: + date = add_months(monthly_schedule_date, r) + amount = depreciation_amount / month_range - self.append("schedules", { - "schedule_date": date, - "depreciation_amount": amount, - "depreciation_method": finance_book.depreciation_method, - "finance_book": finance_book.finance_book, - "finance_book_id": finance_book.idx - }) - else: - self.append("schedules", { - "schedule_date": schedule_date, - "depreciation_amount": depreciation_amount, - "depreciation_method": finance_book.depreciation_method, - "finance_book": finance_book.finance_book, - "finance_book_id": finance_book.idx - }) + self._add_depreciation_row(date, amount, finance_book.depreciation_method, + finance_book.finance_book, finance_book.idx) + else: + self._add_depreciation_row(schedule_date, depreciation_amount, finance_book.depreciation_method, + finance_book.finance_book, finance_book.idx) + + def _add_depreciation_row(self, schedule_date, depreciation_amount, depreciation_method, finance_book, finance_book_id): + self.append("schedules", { + "schedule_date": schedule_date, + "depreciation_amount": depreciation_amount, + "depreciation_method": depreciation_method, + "finance_book": finance_book, + "finance_book_id": finance_book_id + }) + + def _get_value_after_depreciation(self, finance_book): + # value_after_depreciation - current Asset value + if self.docstatus == 1 and finance_book.value_after_depreciation: + value_after_depreciation = flt(finance_book.value_after_depreciation) + else: + value_after_depreciation = (flt(self.gross_purchase_amount) - + flt(self.opening_accumulated_depreciation)) + + return value_after_depreciation # depreciation schedules need to be cleared before modification due to increase in asset life/asset sales # JE: Journal Entry, FB: Finance Book @@ -348,7 +349,6 @@ class Asset(AccountsController): depr_schedule = [] for schedule in self.get('schedules'): - # to update start when there are JEs linked with all the schedule rows corresponding to an FB if len(start) == (int(schedule.finance_book_id) - 2): start.append(num_of_depreciations_completed) @@ -924,3 +924,113 @@ def get_depreciation_amount(asset, depreciable_value, row): depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100)) return depreciation_amount + +@frappe.whitelist() +def split_asset(asset_name, split_qty): + asset = frappe.get_doc("Asset", asset_name) + split_qty = cint(split_qty) + + if split_qty >= asset.asset_quantity: + frappe.throw(_("Split qty cannot be grater than or equal to asset qty")) + + remaining_qty = asset.asset_quantity - split_qty + + new_asset = create_new_asset_after_split(asset, split_qty) + update_existing_asset(asset, remaining_qty) + + return new_asset + +def update_existing_asset(asset, remaining_qty): + remaining_gross_purchase_amount = flt((asset.gross_purchase_amount * remaining_qty) / asset.asset_quantity) + opening_accumulated_depreciation = flt((asset.opening_accumulated_depreciation * remaining_qty) / asset.asset_quantity) + + frappe.db.set_value("Asset", asset.name, { + 'opening_accumulated_depreciation': opening_accumulated_depreciation, + 'gross_purchase_amount': remaining_gross_purchase_amount, + 'asset_quantity': remaining_qty + }) + + for finance_book in asset.get('finance_books'): + value_after_depreciation = flt((finance_book.value_after_depreciation * remaining_qty)/asset.asset_quantity) + expected_value_after_useful_life = flt((finance_book.expected_value_after_useful_life * remaining_qty)/asset.asset_quantity) + frappe.db.set_value('Asset Finance Book', finance_book.name, 'value_after_depreciation', value_after_depreciation) + frappe.db.set_value('Asset Finance Book', finance_book.name, 'expected_value_after_useful_life', expected_value_after_useful_life) + + accumulated_depreciation = 0 + + for term in asset.get('schedules'): + depreciation_amount = flt((term.depreciation_amount * remaining_qty)/asset.asset_quantity) + frappe.db.set_value('Depreciation Schedule', term.name, 'depreciation_amount', depreciation_amount) + accumulated_depreciation += depreciation_amount + frappe.db.set_value('Depreciation Schedule', term.name, 'accumulated_depreciation_amount', accumulated_depreciation) + +def create_new_asset_after_split(asset, split_qty): + new_asset = frappe.copy_doc(asset) + new_gross_purchase_amount = flt((asset.gross_purchase_amount * split_qty) / asset.asset_quantity) + opening_accumulated_depreciation = flt((asset.opening_accumulated_depreciation * split_qty) / asset.asset_quantity) + + new_asset.gross_purchase_amount = new_gross_purchase_amount + new_asset.opening_accumulated_depreciation = opening_accumulated_depreciation + new_asset.asset_quantity = split_qty + new_asset.split_from = asset.name + accumulated_depreciation = 0 + + for finance_book in new_asset.get('finance_books'): + finance_book.value_after_depreciation = flt((finance_book.value_after_depreciation * split_qty)/asset.asset_quantity) + finance_book.expected_value_after_useful_life = flt((finance_book.expected_value_after_useful_life * split_qty)/asset.asset_quantity) + + for term in new_asset.get('schedules'): + depreciation_amount = flt((term.depreciation_amount * split_qty)/asset.asset_quantity) + term.depreciation_amount = depreciation_amount + accumulated_depreciation += depreciation_amount + term.accumulated_depreciation_amount = accumulated_depreciation + + new_asset.submit() + new_asset.set_status() + + for term in new_asset.get('schedules'): + # Update references in JV + if term.journal_entry: + add_reference_in_jv_on_split(term.journal_entry, new_asset.name, asset.name, term.depreciation_amount) + + return new_asset + +def add_reference_in_jv_on_split(entry_name, new_asset_name, old_asset_name, depreciation_amount): + journal_entry = frappe.get_doc('Journal Entry', entry_name) + entries_to_add = [] + idx = len(journal_entry.get('accounts')) + 1 + + for account in journal_entry.get('accounts'): + if account.reference_name == old_asset_name: + entries_to_add.append(frappe.copy_doc(account).as_dict()) + if account.credit: + account.credit = account.credit - depreciation_amount + account.credit_in_account_currency = account.credit_in_account_currency - \ + account.exchange_rate * depreciation_amount + elif account.debit: + account.debit = account.debit - depreciation_amount + account.debit_in_account_currency = account.debit_in_account_currency - \ + account.exchange_rate * depreciation_amount + + for entry in entries_to_add: + entry.reference_name = new_asset_name + if entry.credit: + entry.credit = depreciation_amount + entry.credit_in_account_currency = entry.exchange_rate * depreciation_amount + elif entry.debit: + entry.debit = depreciation_amount + entry.debit_in_account_currency = entry.exchange_rate * depreciation_amount + + entry.idx = idx + idx += 1 + + journal_entry.append('accounts', entry) + + journal_entry.flags.ignore_validate_update_after_submit = True + journal_entry.save() + + # Repost GL Entries + journal_entry.docstatus = 2 + journal_entry.make_gl_entries(1) + journal_entry.docstatus = 1 + journal_entry.make_gl_entries() \ No newline at end of file diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index b9545f46a9..c08dc21a8f 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -7,7 +7,7 @@ import frappe from frappe.utils import add_days, add_months, cstr, flt, get_last_day, getdate, nowdate from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice -from erpnext.assets.doctype.asset.asset import make_sales_invoice +from erpnext.assets.doctype.asset.asset import make_sales_invoice, split_asset from erpnext.assets.doctype.asset.depreciation import ( post_depreciation_entries, restore_asset, @@ -245,6 +245,57 @@ class TestAsset(AssetSetup): si.cancel() self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated") + def test_asset_splitting(self): + asset = create_asset( + calculate_depreciation = 1, + asset_quantity=10, + available_for_use_date = '2020-01-01', + purchase_date = '2020-01-01', + expected_value_after_useful_life = 0, + total_number_of_depreciations = 6, + number_of_depreciations_booked = 1, + frequency_of_depreciation = 10, + depreciation_start_date = '2021-01-01', + opening_accumulated_depreciation=20000, + gross_purchase_amount=120000, + submit = 1 + ) + + post_depreciation_entries(date="2021-01-01") + + self.assertEqual(asset.asset_quantity, 10) + self.assertEqual(asset.gross_purchase_amount, 120000) + self.assertEqual(asset.opening_accumulated_depreciation, 20000) + + new_asset = split_asset(asset.name, 2) + asset.load_from_db() + + self.assertEqual(new_asset.asset_quantity, 2) + self.assertEqual(new_asset.gross_purchase_amount, 24000) + self.assertEqual(new_asset.opening_accumulated_depreciation, 4000) + self.assertEqual(new_asset.split_from, asset.name) + self.assertEqual(new_asset.schedules[0].depreciation_amount, 4000) + self.assertEqual(new_asset.schedules[1].depreciation_amount, 4000) + + self.assertEqual(asset.asset_quantity, 8) + self.assertEqual(asset.gross_purchase_amount, 96000) + self.assertEqual(asset.opening_accumulated_depreciation, 16000) + self.assertEqual(asset.schedules[0].depreciation_amount, 16000) + self.assertEqual(asset.schedules[1].depreciation_amount, 16000) + + journal_entry = asset.schedules[0].journal_entry + + jv = frappe.get_doc('Journal Entry', journal_entry) + self.assertEqual(jv.accounts[0].credit_in_account_currency, 16000) + self.assertEqual(jv.accounts[1].debit_in_account_currency, 16000) + self.assertEqual(jv.accounts[2].credit_in_account_currency, 4000) + self.assertEqual(jv.accounts[3].debit_in_account_currency, 4000) + + self.assertEqual(jv.accounts[0].reference_name, asset.name) + self.assertEqual(jv.accounts[1].reference_name, asset.name) + self.assertEqual(jv.accounts[2].reference_name, new_asset.name) + self.assertEqual(jv.accounts[3].reference_name, new_asset.name) + def test_expense_head(self): pr = make_purchase_receipt(item_code="Macbook Pro", qty=2, rate=200000.0, location="Test Location") @@ -1197,7 +1248,8 @@ def create_asset(**args): "available_for_use_date": args.available_for_use_date or "2020-06-06", "location": args.location or "Test Location", "asset_owner": args.asset_owner or "Company", - "is_existing_asset": args.is_existing_asset or 1 + "is_existing_asset": args.is_existing_asset or 1, + "asset_quantity": args.get("asset_quantity") or 1 }) if asset.calculate_depreciation: