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)