diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index d378fbd26ade..58fd6d4ef8ae 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -214,30 +214,43 @@ frappe.ui.form.on('Asset', { }) }, - render_depreciation_schedule_view: function(frm, depr_schedule) { + render_depreciation_schedule_view: function(frm, asset_depr_schedule_doc) { let wrapper = $(frm.fields_dict["depreciation_schedule_view"].wrapper).empty(); let data = []; - depr_schedule.forEach((sch) => { + asset_depr_schedule_doc.depreciation_schedule.forEach((sch) => { const row = [ sch['idx'], frappe.format(sch['schedule_date'], { fieldtype: 'Date' }), frappe.format(sch['depreciation_amount'], { fieldtype: 'Currency' }), frappe.format(sch['accumulated_depreciation_amount'], { fieldtype: 'Currency' }), - sch['journal_entry'] || '' + sch['journal_entry'] || '', ]; + + if (asset_depr_schedule_doc.shift_based) { + row.push(sch['shift']); + } + data.push(row); }); + let columns = [ + {name: __("No."), editable: false, resizable: false, format: value => value, width: 60}, + {name: __("Schedule Date"), editable: false, resizable: false, width: 270}, + {name: __("Depreciation Amount"), editable: false, resizable: false, width: 164}, + {name: __("Accumulated Depreciation Amount"), editable: false, resizable: false, width: 164}, + ]; + + if (asset_depr_schedule_doc.shift_based) { + columns.push({name: __("Journal Entry"), editable: false, resizable: false, format: value => `${value}`, width: 245}); + columns.push({name: __("Shift"), editable: false, resizable: false, width: 59}); + } else { + columns.push({name: __("Journal Entry"), editable: false, resizable: false, format: value => `${value}`, width: 304}); + } + let datatable = new frappe.DataTable(wrapper.get(0), { - columns: [ - {name: __("No."), editable: false, resizable: false, format: value => value, width: 60}, - {name: __("Schedule Date"), editable: false, resizable: false, width: 270}, - {name: __("Depreciation Amount"), editable: false, resizable: false, width: 164}, - {name: __("Accumulated Depreciation Amount"), editable: false, resizable: false, width: 164}, - {name: __("Journal Entry"), editable: false, resizable: false, format: value => `${value}`, width: 304} - ], + columns: columns, data: data, layout: "fluid", serialNoColumn: false, @@ -272,8 +285,8 @@ frappe.ui.form.on('Asset', { asset_values.push(flt(frm.doc.gross_purchase_amount - frm.doc.opening_accumulated_depreciation, precision('gross_purchase_amount'))); } - let depr_schedule = (await frappe.call( - "erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule.get_depr_schedule", + let asset_depr_schedule_doc = (await frappe.call( + "erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule.get_asset_depr_schedule_doc", { asset_name: frm.doc.name, status: "Active", @@ -281,7 +294,7 @@ frappe.ui.form.on('Asset', { } )).message; - $.each(depr_schedule || [], function(i, v) { + $.each(asset_depr_schedule_doc.depreciation_schedule || [], function(i, v) { x_intervals.push(frappe.format(v.schedule_date, { fieldtype: 'Date' })); var asset_value = flt(frm.doc.gross_purchase_amount - v.accumulated_depreciation_amount, precision('gross_purchase_amount')); if(v.journal_entry) { @@ -296,7 +309,7 @@ frappe.ui.form.on('Asset', { }); frm.toggle_display(["depreciation_schedule_view"], 1); - frm.events.render_depreciation_schedule_view(frm, depr_schedule); + frm.events.render_depreciation_schedule_view(frm, asset_depr_schedule_doc); } else { if(frm.doc.opening_accumulated_depreciation) { x_intervals.push(frappe.format(frm.doc.creation.split(" ")[0], { fieldtype: 'Date' })); diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 7b7953b6ce6b..d1f03f20462d 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -825,6 +825,7 @@ def get_item_details(item_code, asset_category, gross_purchase_amount): "total_number_of_depreciations": d.total_number_of_depreciations, "frequency_of_depreciation": d.frequency_of_depreciation, "daily_prorata_based": d.daily_prorata_based, + "shift_based": d.shift_based, "salvage_value_percentage": d.salvage_value_percentage, "expected_value_after_useful_life": flt(gross_purchase_amount) * flt(d.salvage_value_percentage / 100), diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index ca2b9805790a..dc80aa5c5c71 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -1000,7 +1000,11 @@ def test_get_depreciation_amount(self): }, ) - depreciation_amount = get_depreciation_amount(asset, 100000, asset.finance_books[0]) + asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active") + + depreciation_amount = get_depreciation_amount( + asset_depr_schedule_doc, asset, 100000, asset.finance_books[0] + ) self.assertEqual(depreciation_amount, 30000) def test_make_depr_schedule(self): @@ -1732,6 +1736,7 @@ def create_asset(**args): "expected_value_after_useful_life": args.expected_value_after_useful_life or 0, "depreciation_start_date": args.depreciation_start_date, "daily_prorata_based": args.daily_prorata_based or 0, + "shift_based": args.shift_based or 0, }, ) diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.js b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.js index 3d2dff179aa0..c99297d62663 100644 --- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.js +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.js @@ -8,11 +8,13 @@ frappe.ui.form.on('Asset Depreciation Schedule', { }, make_schedules_editable: function(frm) { - var is_editable = frm.doc.depreciation_method == "Manual" ? true : false; + var is_manual_hence_editable = frm.doc.depreciation_method === "Manual" ? true : false; + var is_shift_hence_editable = frm.doc.shift_based ? true : false; - frm.toggle_enable("depreciation_schedule", is_editable); - frm.fields_dict["depreciation_schedule"].grid.toggle_enable("schedule_date", is_editable); - frm.fields_dict["depreciation_schedule"].grid.toggle_enable("depreciation_amount", is_editable); + frm.toggle_enable("depreciation_schedule", is_manual_hence_editable || is_shift_hence_editable); + frm.fields_dict["depreciation_schedule"].grid.toggle_enable("schedule_date", is_manual_hence_editable); + frm.fields_dict["depreciation_schedule"].grid.toggle_enable("depreciation_amount", is_manual_hence_editable); + frm.fields_dict["depreciation_schedule"].grid.toggle_enable("shift", is_shift_hence_editable); } }); diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json index 8d8b46321fc3..be35914251d8 100644 --- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json @@ -20,6 +20,7 @@ "total_number_of_depreciations", "rate_of_depreciation", "daily_prorata_based", + "shift_based", "column_break_8", "frequency_of_depreciation", "expected_value_after_useful_life", @@ -184,12 +185,20 @@ "label": "Depreciate based on daily pro-rata", "print_hide": 1, "read_only": 1 + }, + { + "default": "0", + "depends_on": "eval:doc.depreciation_method == \"Straight Line\"", + "fieldname": "shift_based", + "fieldtype": "Check", + "label": "Depreciate based on shifts", + "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-11-03 21:32:15.021796", + "modified": "2023-11-29 00:57:00.461998", "modified_by": "Administrator", "module": "Assets", "name": "Asset Depreciation Schedule", 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 7305691f97c4..6e390ce6f81d 100644 --- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py @@ -26,6 +26,7 @@ def before_save(self): 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() @@ -73,6 +74,16 @@ def cancel_depreciation_entries(self): 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 + + asset_doc = frappe.get_doc("Asset", self.asset) + fb_row = asset_doc.finance_books[self.finance_book_id - 1] + + 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) @@ -154,13 +165,14 @@ def set_draft_asset_depr_schedule_details(self, asset_doc, row): 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 self.status = "Draft" def make_depr_schedule( self, asset_doc, row, - date_of_disposal, + date_of_disposal=None, update_asset_finance_book_row=True, value_after_depreciation=None, ): @@ -181,6 +193,8 @@ def clear_depr_schedule(self): 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 @@ -246,6 +260,7 @@ def _make_depr_schedule( prev_depreciation_amount = 0 depreciation_amount = get_depreciation_amount( + self, asset_doc, value_after_depreciation, row, @@ -282,10 +297,7 @@ def _make_depr_schedule( ) if depreciation_amount > 0: - self.add_depr_schedule_row( - date_of_disposal, - depreciation_amount, - ) + self.add_depr_schedule_row(date_of_disposal, depreciation_amount, n) break @@ -369,10 +381,7 @@ def _make_depr_schedule( skip_row = True if flt(depreciation_amount, asset_doc.precision("gross_purchase_amount")) > 0: - self.add_depr_schedule_row( - schedule_date, - depreciation_amount, - ) + self.add_depr_schedule_row(schedule_date, depreciation_amount, n) # to ensure that final accumulated depreciation amount is accurate def get_adjusted_depreciation_amount( @@ -394,16 +403,22 @@ def get_adjusted_depreciation_amount( def get_depreciation_amount_for_first_row(self): return self.get("depreciation_schedule")[0].depreciation_amount - def add_depr_schedule_row( - self, - schedule_date, - depreciation_amount, - ): + 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, }, ) @@ -445,6 +460,7 @@ def set_accumulated_depreciation( and i == max(straight_line_idx) - 1 and not date_of_disposal and not date_of_return + and not row.shift_based ): depreciation_amount += flt( value_after_depreciation - flt(row.expected_value_after_useful_life), @@ -527,6 +543,7 @@ def get_total_days(date, frequency): def get_depreciation_amount( + asset_depr_schedule, asset, depreciable_value, fb_row, @@ -537,7 +554,7 @@ def get_depreciation_amount( ): if fb_row.depreciation_method in ("Straight Line", "Manual"): return get_straight_line_or_manual_depr_amount( - asset, fb_row, schedule_idx, number_of_pending_depreciations + asset_depr_schedule, asset, fb_row, schedule_idx, number_of_pending_depreciations ) else: rate_of_depreciation = get_updated_rate_of_depreciation_for_wdv_and_dd( @@ -559,8 +576,11 @@ def get_updated_rate_of_depreciation_for_wdv_and_dd(asset, depreciable_value, fb def get_straight_line_or_manual_depr_amount( - asset, row, schedule_idx, number_of_pending_depreciations + asset_depr_schedule, asset, row, schedule_idx, number_of_pending_depreciations ): + if row.shift_based: + return get_shift_depr_amount(asset_depr_schedule, asset, row, schedule_idx) + # if the Depreciation Schedule is being modified after Asset Repair due to increase in asset life and value if asset.flags.increase_in_asset_life: return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / ( @@ -655,6 +675,41 @@ def get_straight_line_or_manual_depr_amount( ) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked) +def get_shift_depr_amount(asset_depr_schedule, asset, 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(row.expected_value_after_useful_life) + ) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked) + + 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(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)) + + def get_wdv_or_dd_depr_amount( depreciable_value, rate_of_depreciation, @@ -803,7 +858,12 @@ def make_new_active_asset_depr_schedules_and_cancel_current_ones( def get_temp_asset_depr_schedule_doc( - asset_doc, row, date_of_disposal=None, date_of_return=None, update_asset_finance_book_row=False + asset_doc, + row, + date_of_disposal=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 @@ -818,6 +878,21 @@ def get_temp_asset_depr_schedule_doc( temp_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc) + if new_depr_schedule: + temp_asset_depr_schedule_doc.depreciation_schedule = [] + + for schedule in new_depr_schedule: + temp_asset_depr_schedule_doc.append( + "depreciation_schedule", + { + "schedule_date": schedule.schedule_date, + "depreciation_amount": schedule.depreciation_amount, + "accumulated_depreciation_amount": schedule.accumulated_depreciation_amount, + "journal_entry": schedule.journal_entry, + "shift": schedule.shift, + }, + ) + temp_asset_depr_schedule_doc.prepare_draft_asset_depr_schedule_data( asset_doc, row, @@ -839,6 +914,7 @@ def get_depr_schedule(asset_name, status, finance_book=None): return asset_depr_schedule_doc.get("depreciation_schedule") +@frappe.whitelist() def get_asset_depr_schedule_doc(asset_name, status, finance_book=None): asset_depr_schedule_name = get_asset_depr_schedule_name(asset_name, status, finance_book) 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 e597d5fe31e6..25ae7a492c8c 100644 --- a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json +++ b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json @@ -9,6 +9,7 @@ "depreciation_method", "total_number_of_depreciations", "daily_prorata_based", + "shift_based", "column_break_5", "frequency_of_depreciation", "depreciation_start_date", @@ -97,12 +98,19 @@ "fieldname": "daily_prorata_based", "fieldtype": "Check", "label": "Depreciate based on daily pro-rata" + }, + { + "default": "0", + "depends_on": "eval:doc.depreciation_method == \"Straight Line\"", + "fieldname": "shift_based", + "fieldtype": "Check", + "label": "Depreciate based on shifts" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-11-03 21:30:24.266601", + "modified": "2023-11-29 00:57:07.579777", "modified_by": "Administrator", "module": "Assets", "name": "Asset Finance Book", diff --git a/erpnext/assets/doctype/asset_shift_allocation/__init__.py b/erpnext/assets/doctype/asset_shift_allocation/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.js b/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.js new file mode 100644 index 000000000000..54df69339b56 --- /dev/null +++ b/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.js @@ -0,0 +1,14 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +frappe.ui.form.on('Asset Shift Allocation', { + onload: function(frm) { + frm.events.make_schedules_editable(frm); + }, + + make_schedules_editable: function(frm) { + frm.toggle_enable("depreciation_schedule", true); + frm.fields_dict["depreciation_schedule"].grid.toggle_enable("schedule_date", false); + frm.fields_dict["depreciation_schedule"].grid.toggle_enable("depreciation_amount", false); + frm.fields_dict["depreciation_schedule"].grid.toggle_enable("shift", true); + } +}); diff --git a/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.json b/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.json new file mode 100644 index 000000000000..89fa298a74ac --- /dev/null +++ b/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.json @@ -0,0 +1,111 @@ +{ + "actions": [], + "autoname": "naming_series:", + "creation": "2023-11-24 15:07:44.652133", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "section_break_esaa", + "asset", + "naming_series", + "column_break_tdae", + "finance_book", + "amended_from", + "depreciation_schedule_section", + "depreciation_schedule" + ], + "fields": [ + { + "fieldname": "section_break_esaa", + "fieldtype": "Section Break" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Asset Shift Allocation", + "print_hide": 1, + "read_only": 1, + "search_index": 1 + }, + { + "fieldname": "asset", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Asset", + "options": "Asset", + "reqd": 1 + }, + { + "fieldname": "column_break_tdae", + "fieldtype": "Column Break" + }, + { + "fieldname": "finance_book", + "fieldtype": "Link", + "label": "Finance Book", + "options": "Finance Book" + }, + { + "depends_on": "eval:!doc.__islocal", + "fieldname": "depreciation_schedule_section", + "fieldtype": "Section Break", + "label": "Depreciation Schedule" + }, + { + "fieldname": "depreciation_schedule", + "fieldtype": "Table", + "label": "Depreciation Schedule", + "options": "Depreciation Schedule" + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Naming Series", + "options": "ACC-ASA-.YYYY.-", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2023-11-29 04:05:04.683518", + "modified_by": "Administrator", + "module": "Assets", + "name": "Asset Shift Allocation", + "naming_rule": "By \"Naming Series\" field", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1, + "submit": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.py b/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.py new file mode 100644 index 000000000000..d419ef4c8416 --- /dev/null +++ b/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.py @@ -0,0 +1,262 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.model.document import Document +from frappe.utils import ( + add_months, + cint, + flt, + get_last_day, + get_link_to_form, + is_last_day_of_the_month, +) + +from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity +from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( + get_asset_depr_schedule_doc, + get_asset_shift_factors_map, + get_temp_asset_depr_schedule_doc, +) + + +class AssetShiftAllocation(Document): + def after_insert(self): + self.fetch_and_set_depr_schedule() + + def validate(self): + self.asset_depr_schedule_doc = get_asset_depr_schedule_doc( + self.asset, "Active", self.finance_book + ) + + self.validate_invalid_shift_change() + self.update_depr_schedule() + + def on_submit(self): + self.create_new_asset_depr_schedule() + + def fetch_and_set_depr_schedule(self): + if self.asset_depr_schedule_doc: + if self.asset_depr_schedule_doc.shift_based: + for schedule in self.asset_depr_schedule_doc.get("depreciation_schedule"): + self.append( + "depreciation_schedule", + { + "schedule_date": schedule.schedule_date, + "depreciation_amount": schedule.depreciation_amount, + "accumulated_depreciation_amount": schedule.accumulated_depreciation_amount, + "journal_entry": schedule.journal_entry, + "shift": schedule.shift, + }, + ) + + self.flags.ignore_validate = True + self.save() + else: + frappe.throw( + _( + "Asset Depreciation Schedule for Asset {0} and Finance Book {1} is not using shift based depreciation" + ).format(self.asset, self.finance_book) + ) + else: + frappe.throw( + _("Asset Depreciation Schedule not found for Asset {0} and Finance Book {1}").format( + self.asset, self.finance_book + ) + ) + + def validate_invalid_shift_change(self): + if not self.get("depreciation_schedule") or self.docstatus == 1: + return + + for i, sch in enumerate(self.depreciation_schedule): + if ( + sch.journal_entry and self.asset_depr_schedule_doc.depreciation_schedule[i].shift != sch.shift + ): + frappe.throw( + _( + "Row {0}: Shift cannot be changed since the depreciation has already been processed" + ).format(i) + ) + + def update_depr_schedule(self): + if not self.get("depreciation_schedule") or self.docstatus == 1: + return + + self.allocate_shift_diff_in_depr_schedule() + + asset_doc = frappe.get_doc("Asset", self.asset) + fb_row = asset_doc.finance_books[self.asset_depr_schedule_doc.finance_book_id - 1] + + asset_doc.flags.shift_allocation = True + + temp_depr_schedule = get_temp_asset_depr_schedule_doc( + asset_doc, fb_row, new_depr_schedule=self.depreciation_schedule + ).get("depreciation_schedule") + + self.depreciation_schedule = [] + + for schedule in temp_depr_schedule: + self.append( + "depreciation_schedule", + { + "schedule_date": schedule.schedule_date, + "depreciation_amount": schedule.depreciation_amount, + "accumulated_depreciation_amount": schedule.accumulated_depreciation_amount, + "journal_entry": schedule.journal_entry, + "shift": schedule.shift, + }, + ) + + def allocate_shift_diff_in_depr_schedule(self): + asset_shift_factors_map = get_asset_shift_factors_map() + reverse_asset_shift_factors_map = { + asset_shift_factors_map[k]: k for k in asset_shift_factors_map + } + + original_shift_factors_sum = sum( + flt(asset_shift_factors_map.get(schedule.shift)) + for schedule in self.asset_depr_schedule_doc.depreciation_schedule + ) + + new_shift_factors_sum = sum( + flt(asset_shift_factors_map.get(schedule.shift)) for schedule in self.depreciation_schedule + ) + + diff = new_shift_factors_sum - original_shift_factors_sum + + if diff > 0: + for i, schedule in reversed(list(enumerate(self.depreciation_schedule))): + if diff <= 0: + break + + shift_factor = flt(asset_shift_factors_map.get(schedule.shift)) + + if shift_factor <= diff: + self.depreciation_schedule.pop() + diff -= shift_factor + else: + try: + self.depreciation_schedule[i].shift = reverse_asset_shift_factors_map.get( + shift_factor - diff + ) + diff = 0 + except Exception: + frappe.throw(_("Could not auto update shifts. Shift with shift factor {0} needed.")).format( + shift_factor - diff + ) + elif diff < 0: + shift_factors = list(asset_shift_factors_map.values()) + desc_shift_factors = sorted(shift_factors, reverse=True) + depr_schedule_len_diff = self.asset_depr_schedule_doc.total_number_of_depreciations - len( + self.depreciation_schedule + ) + subsets_result = [] + + if depr_schedule_len_diff > 0: + num_rows_to_add = depr_schedule_len_diff + + while not subsets_result and num_rows_to_add > 0: + find_subsets_with_sum(shift_factors, num_rows_to_add, abs(diff), [], subsets_result) + if subsets_result: + break + num_rows_to_add -= 1 + + if subsets_result: + for i in range(num_rows_to_add): + schedule_date = add_months( + self.depreciation_schedule[-1].schedule_date, + cint(self.asset_depr_schedule_doc.frequency_of_depreciation), + ) + + if is_last_day_of_the_month(self.depreciation_schedule[-1].schedule_date): + schedule_date = get_last_day(schedule_date) + + self.append( + "depreciation_schedule", + { + "schedule_date": schedule_date, + "shift": reverse_asset_shift_factors_map.get(subsets_result[0][i]), + }, + ) + + if depr_schedule_len_diff <= 0 or not subsets_result: + for i, schedule in reversed(list(enumerate(self.depreciation_schedule))): + diff = abs(diff) + + if diff <= 0: + break + + shift_factor = flt(asset_shift_factors_map.get(schedule.shift)) + + if shift_factor <= diff: + for sf in desc_shift_factors: + if sf - shift_factor <= diff: + self.depreciation_schedule[i].shift = reverse_asset_shift_factors_map.get(sf) + diff -= sf - shift_factor + break + else: + try: + self.depreciation_schedule[i].shift = reverse_asset_shift_factors_map.get( + shift_factor + diff + ) + diff = 0 + except Exception: + frappe.throw(_("Could not auto update shifts. Shift with shift factor {0} needed.")).format( + shift_factor + diff + ) + + def create_new_asset_depr_schedule(self): + new_asset_depr_schedule_doc = frappe.copy_doc(self.asset_depr_schedule_doc) + + new_asset_depr_schedule_doc.depreciation_schedule = [] + + for schedule in self.depreciation_schedule: + new_asset_depr_schedule_doc.append( + "depreciation_schedule", + { + "schedule_date": schedule.schedule_date, + "depreciation_amount": schedule.depreciation_amount, + "accumulated_depreciation_amount": schedule.accumulated_depreciation_amount, + "journal_entry": schedule.journal_entry, + "shift": schedule.shift, + }, + ) + + notes = _( + "This schedule was created when Asset {0}'s shifts were adjusted through Asset Shift Allocation {1}." + ).format( + get_link_to_form("Asset", self.asset), + get_link_to_form(self.doctype, self.name), + ) + + new_asset_depr_schedule_doc.notes = notes + + self.asset_depr_schedule_doc.flags.should_not_cancel_depreciation_entries = True + self.asset_depr_schedule_doc.cancel() + + new_asset_depr_schedule_doc.submit() + + add_asset_activity( + self.asset, + _("Asset's depreciation schedule updated after Asset Shift Allocation {0}").format( + get_link_to_form(self.doctype, self.name) + ), + ) + + +def find_subsets_with_sum(numbers, k, target_sum, current_subset, result): + if k == 0 and target_sum == 0: + result.append(current_subset.copy()) + return + if k <= 0 or target_sum <= 0 or not numbers: + return + + # Include the current number in the subset + find_subsets_with_sum( + numbers, k - 1, target_sum - numbers[0], current_subset + [numbers[0]], result + ) + + # Exclude the current number from the subset + find_subsets_with_sum(numbers[1:], k, target_sum, current_subset, result) diff --git a/erpnext/assets/doctype/asset_shift_allocation/test_asset_shift_allocation.py b/erpnext/assets/doctype/asset_shift_allocation/test_asset_shift_allocation.py new file mode 100644 index 000000000000..8d00a24f6b2f --- /dev/null +++ b/erpnext/assets/doctype/asset_shift_allocation/test_asset_shift_allocation.py @@ -0,0 +1,113 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe.tests.utils import FrappeTestCase +from frappe.utils import cstr + +from erpnext.assets.doctype.asset.test_asset import create_asset +from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( + get_depr_schedule, +) + + +class TestAssetShiftAllocation(FrappeTestCase): + @classmethod + def setUpClass(cls): + create_asset_shift_factors() + + @classmethod + def tearDownClass(cls): + frappe.db.rollback() + + def test_asset_shift_allocation(self): + asset = create_asset( + calculate_depreciation=1, + available_for_use_date="2023-01-01", + purchase_date="2023-01-01", + gross_purchase_amount=120000, + depreciation_start_date="2023-01-31", + total_number_of_depreciations=12, + frequency_of_depreciation=1, + shift_based=1, + submit=1, + ) + + expected_schedules = [ + ["2023-01-31", 10000.0, 10000.0, "Single"], + ["2023-02-28", 10000.0, 20000.0, "Single"], + ["2023-03-31", 10000.0, 30000.0, "Single"], + ["2023-04-30", 10000.0, 40000.0, "Single"], + ["2023-05-31", 10000.0, 50000.0, "Single"], + ["2023-06-30", 10000.0, 60000.0, "Single"], + ["2023-07-31", 10000.0, 70000.0, "Single"], + ["2023-08-31", 10000.0, 80000.0, "Single"], + ["2023-09-30", 10000.0, 90000.0, "Single"], + ["2023-10-31", 10000.0, 100000.0, "Single"], + ["2023-11-30", 10000.0, 110000.0, "Single"], + ["2023-12-31", 10000.0, 120000.0, "Single"], + ] + + schedules = [ + [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount, d.shift] + for d in get_depr_schedule(asset.name, "Active") + ] + + self.assertEqual(schedules, expected_schedules) + + asset_shift_allocation = frappe.get_doc( + {"doctype": "Asset Shift Allocation", "asset": asset.name} + ).insert() + + schedules = [ + [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount, d.shift] + for d in asset_shift_allocation.get("depreciation_schedule") + ] + + self.assertEqual(schedules, expected_schedules) + + asset_shift_allocation = frappe.get_doc("Asset Shift Allocation", asset_shift_allocation.name) + asset_shift_allocation.depreciation_schedule[4].shift = "Triple" + asset_shift_allocation.save() + + schedules = [ + [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount, d.shift] + for d in asset_shift_allocation.get("depreciation_schedule") + ] + + expected_schedules = [ + ["2023-01-31", 10000.0, 10000.0, "Single"], + ["2023-02-28", 10000.0, 20000.0, "Single"], + ["2023-03-31", 10000.0, 30000.0, "Single"], + ["2023-04-30", 10000.0, 40000.0, "Single"], + ["2023-05-31", 20000.0, 60000.0, "Triple"], + ["2023-06-30", 10000.0, 70000.0, "Single"], + ["2023-07-31", 10000.0, 80000.0, "Single"], + ["2023-08-31", 10000.0, 90000.0, "Single"], + ["2023-09-30", 10000.0, 100000.0, "Single"], + ["2023-10-31", 10000.0, 110000.0, "Single"], + ["2023-11-30", 10000.0, 120000.0, "Single"], + ] + + self.assertEqual(schedules, expected_schedules) + + asset_shift_allocation.submit() + + schedules = [ + [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount, d.shift] + for d in get_depr_schedule(asset.name, "Active") + ] + + self.assertEqual(schedules, expected_schedules) + + +def create_asset_shift_factors(): + shifts = [ + {"doctype": "Asset Shift Factor", "shift_name": "Half", "shift_factor": 0.5, "default": 0}, + {"doctype": "Asset Shift Factor", "shift_name": "Single", "shift_factor": 1, "default": 1}, + {"doctype": "Asset Shift Factor", "shift_name": "Double", "shift_factor": 1.5, "default": 0}, + {"doctype": "Asset Shift Factor", "shift_name": "Triple", "shift_factor": 2, "default": 0}, + ] + + for s in shifts: + frappe.get_doc(s).insert() diff --git a/erpnext/assets/doctype/asset_shift_factor/__init__.py b/erpnext/assets/doctype/asset_shift_factor/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.js b/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.js new file mode 100644 index 000000000000..88887fea8774 --- /dev/null +++ b/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.js @@ -0,0 +1,8 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Asset Shift Factor", { +// refresh(frm) { + +// }, +// }); diff --git a/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.json b/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.json new file mode 100644 index 000000000000..fd04ffc5d444 --- /dev/null +++ b/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.json @@ -0,0 +1,74 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "field:shift_name", + "creation": "2023-11-27 18:16:03.980086", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "shift_name", + "shift_factor", + "default" + ], + "fields": [ + { + "fieldname": "shift_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Shift Name", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "shift_factor", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Shift Factor", + "reqd": 1 + }, + { + "default": "0", + "fieldname": "default", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Default" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2023-11-29 04:04:24.272872", + "modified_by": "Administrator", + "module": "Assets", + "name": "Asset Shift Factor", + "naming_rule": "By fieldname", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.py b/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.py new file mode 100644 index 000000000000..4c275ce092cc --- /dev/null +++ b/erpnext/assets/doctype/asset_shift_factor/asset_shift_factor.py @@ -0,0 +1,24 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.model.document import Document + + +class AssetShiftFactor(Document): + def validate(self): + self.validate_default() + + def validate_default(self): + if self.default: + existing_default_shift_factor = frappe.db.get_value( + "Asset Shift Factor", {"default": 1}, "name" + ) + + if existing_default_shift_factor: + frappe.throw( + _("Asset Shift Factor {0} is set as default currently. Please change it first.").format( + frappe.bold(existing_default_shift_factor) + ) + ) diff --git a/erpnext/assets/doctype/asset_shift_factor/test_asset_shift_factor.py b/erpnext/assets/doctype/asset_shift_factor/test_asset_shift_factor.py new file mode 100644 index 000000000000..75073673c0c2 --- /dev/null +++ b/erpnext/assets/doctype/asset_shift_factor/test_asset_shift_factor.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestAssetShiftFactor(FrappeTestCase): + pass diff --git a/erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json b/erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json index 884e0c6cb2b6..ef706e8f25a8 100644 --- a/erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json +++ b/erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json @@ -12,6 +12,7 @@ "column_break_3", "accumulated_depreciation_amount", "journal_entry", + "shift", "make_depreciation_entry" ], "fields": [ @@ -57,11 +58,17 @@ "fieldname": "make_depreciation_entry", "fieldtype": "Button", "label": "Make Depreciation Entry" + }, + { + "fieldname": "shift", + "fieldtype": "Link", + "label": "Shift", + "options": "Asset Shift Factor" } ], "istable": 1, "links": [], - "modified": "2023-07-26 12:56:48.718736", + "modified": "2023-11-27 18:28:35.325376", "modified_by": "Administrator", "module": "Assets", "name": "Depreciation Schedule", 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 9a2a39fb78c1..793497b766eb 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 @@ -86,6 +86,7 @@ def get_asset_finance_books_map(): afb.frequency_of_depreciation, afb.rate_of_depreciation, afb.expected_value_after_useful_life, + afb.shift_based, ) .where(asset.docstatus < 2) .orderby(afb.idx)