diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json
index 39c914c0c6a..c7430dbf00c 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.json
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json
@@ -89,7 +89,7 @@
"label": "Entry Type",
"oldfieldname": "voucher_type",
"oldfieldtype": "Select",
- "options": "Journal Entry\nInter Company Journal Entry\nBank Entry\nCash Entry\nCredit Card Entry\nDebit Note\nCredit Note\nContra Entry\nExcise Entry\nWrite Off Entry\nOpening Entry\nDepreciation Entry\nExchange Rate Revaluation\nExchange Gain Or Loss\nDeferred Revenue\nDeferred Expense",
+ "options": "Journal Entry\nInter Company Journal Entry\nBank Entry\nCash Entry\nCredit Card Entry\nDebit Note\nCredit Note\nContra Entry\nExcise Entry\nWrite Off Entry\nOpening Entry\nDepreciation Entry\nAsset Disposal\nExchange Rate Revaluation\nExchange Gain Or Loss\nDeferred Revenue\nDeferred Expense",
"reqd": 1,
"search_index": 1
},
@@ -557,7 +557,7 @@
"table_fieldname": "payment_entries"
}
],
- "modified": "2024-07-18 15:32:29.413598",
+ "modified": "2024-12-26 15:32:20.730666",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry",
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index a3047c9339d..d5070db1735 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -100,6 +100,7 @@ class JournalEntry(AccountsController):
"Write Off Entry",
"Opening Entry",
"Depreciation Entry",
+ "Asset Disposal",
"Exchange Rate Revaluation",
"Exchange Gain Or Loss",
"Deferred Revenue",
@@ -140,7 +141,7 @@ class JournalEntry(AccountsController):
self.validate_credit_debit_note()
self.validate_empty_accounts_table()
self.validate_inter_company_accounts()
- self.validate_depr_entry_voucher_type()
+ self.validate_depr_account_and_depr_entry_voucher_type()
self.validate_company_in_accounting_dimension()
self.validate_advance_accounts()
@@ -196,7 +197,6 @@ class JournalEntry(AccountsController):
self.update_asset_value()
self.update_inter_company_jv()
self.update_invoice_discounting()
- self.update_booked_depreciation()
def on_update_after_submit(self):
# Flag will be set on Reconciliation
@@ -232,7 +232,6 @@ class JournalEntry(AccountsController):
self.unlink_inter_company_jv()
self.unlink_asset_adjustment_entry()
self.update_invoice_discounting()
- self.update_booked_depreciation(1)
def get_title(self):
return self.pay_to_recd_from or self.accounts[0].account
@@ -269,12 +268,16 @@ class JournalEntry(AccountsController):
):
frappe.throw(_("Total Credit/ Debit Amount should be same as linked Journal Entry"))
- def validate_depr_entry_voucher_type(self):
- if (
- any(d.account_type == "Depreciation" for d in self.get("accounts"))
- and self.voucher_type != "Depreciation Entry"
- ):
- frappe.throw(_("Journal Entry type should be set as Depreciation Entry for asset depreciation"))
+ def validate_depr_account_and_depr_entry_voucher_type(self):
+ for d in self.get("accounts"):
+ if d.account_type == "Depreciation":
+ if self.voucher_type != "Depreciation Entry":
+ frappe.throw(
+ _("Journal Entry type should be set as Depreciation Entry for asset depreciation")
+ )
+
+ if frappe.get_cached_value("Account", d.account, "root_type") != "Expense":
+ frappe.throw(_("Account {0} should be of type Expense").format(d.account))
def validate_stock_accounts(self):
stock_accounts = get_stock_accounts(self.company, accounts=self.accounts)
@@ -379,7 +382,11 @@ class JournalEntry(AccountsController):
self.remove(d)
def update_asset_value(self):
- if self.flags.planned_depr_entry or self.voucher_type != "Depreciation Entry":
+ self.update_asset_on_depreciation()
+ self.update_asset_on_disposal()
+
+ def update_asset_on_depreciation(self):
+ if self.voucher_type != "Depreciation Entry":
return
for d in self.get("accounts"):
@@ -389,22 +396,59 @@ class JournalEntry(AccountsController):
and d.account_type == "Depreciation"
and d.debit
):
- asset = frappe.get_doc("Asset", d.reference_name)
+ asset = frappe.get_cached_doc("Asset", d.reference_name)
if asset.calculate_depreciation:
- fb_idx = 1
- if self.finance_book:
- for fb_row in asset.get("finance_books"):
- if fb_row.finance_book == self.finance_book:
- fb_idx = fb_row.idx
- break
- fb_row = asset.get("finance_books")[fb_idx - 1]
- fb_row.value_after_depreciation -= d.debit
- fb_row.db_update()
- else:
- asset.db_set("value_after_depreciation", asset.value_after_depreciation - d.debit)
+ self.update_journal_entry_link_on_depr_schedule(asset, d)
+ self.update_value_after_depreciation(asset, d.debit)
+ asset.db_set("value_after_depreciation", asset.value_after_depreciation - d.debit)
asset.set_status()
+ asset.set_total_booked_depreciations()
+
+ def update_value_after_depreciation(self, asset, depr_amount):
+ fb_idx = 1
+ if self.finance_book:
+ for fb_row in asset.get("finance_books"):
+ if fb_row.finance_book == self.finance_book:
+ fb_idx = fb_row.idx
+ break
+ fb_row = asset.get("finance_books")[fb_idx - 1]
+ fb_row.value_after_depreciation -= depr_amount
+ frappe.db.set_value(
+ "Asset Finance Book", fb_row.name, "value_after_depreciation", fb_row.value_after_depreciation
+ )
+
+ def update_journal_entry_link_on_depr_schedule(self, asset, je_row):
+ depr_schedule = get_depr_schedule(asset.name, "Active", self.finance_book)
+ for d in depr_schedule or []:
+ if (
+ d.schedule_date == self.posting_date
+ and not d.journal_entry
+ and d.depreciation_amount == flt(je_row.debit)
+ ):
+ frappe.db.set_value("Depreciation Schedule", d.name, "journal_entry", self.name)
+
+ def update_asset_on_disposal(self):
+ if self.voucher_type == "Asset Disposal":
+ disposed_assets = []
+ for d in self.get("accounts"):
+ if (
+ d.reference_type == "Asset"
+ and d.reference_name
+ and d.reference_name not in disposed_assets
+ ):
+ frappe.db.set_value(
+ "Asset",
+ d.reference_name,
+ {
+ "disposal_date": self.posting_date,
+ "journal_entry_for_scrap": self.name,
+ },
+ )
+ asset_doc = frappe.get_doc("Asset", d.reference_name)
+ asset_doc.set_status()
+ disposed_assets.append(d.reference_name)
def update_inter_company_jv(self):
if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference:
@@ -459,25 +503,6 @@ class JournalEntry(AccountsController):
if status:
inv_disc_doc.set_status(status=status)
- def update_booked_depreciation(self, cancel=0):
- for d in self.get("accounts"):
- if (
- self.voucher_type == "Depreciation Entry"
- and d.reference_type == "Asset"
- and d.reference_name
- and frappe.get_cached_value("Account", d.account, "root_type") == "Expense"
- and d.debit
- ):
- asset = frappe.get_doc("Asset", d.reference_name)
- for fb_row in asset.get("finance_books"):
- if fb_row.finance_book == self.finance_book:
- if cancel:
- fb_row.total_number_of_booked_depreciations -= 1
- else:
- fb_row.total_number_of_booked_depreciations += 1
- fb_row.db_update()
- break
-
def unlink_advance_entry_reference(self):
for d in self.get("accounts"):
if d.is_advance == "Yes" and d.reference_type in ("Sales Invoice", "Purchase Invoice"):
@@ -527,9 +552,9 @@ class JournalEntry(AccountsController):
fb_row = asset.get("finance_books")[fb_idx - 1]
fb_row.value_after_depreciation += d.debit
fb_row.db_update()
- else:
- asset.db_set("value_after_depreciation", asset.value_after_depreciation + d.debit)
+ asset.db_set("value_after_depreciation", asset.value_after_depreciation + d.debit)
asset.set_status()
+ asset.set_total_booked_depreciations()
elif self.voucher_type == "Journal Entry" and d.reference_type == "Asset" and d.reference_name:
journal_entry_for_scrap = frappe.db.get_value(
"Asset", d.reference_name, "journal_entry_for_scrap"
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 8eacada5e4a..62bb9c65dc2 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -39,7 +39,7 @@ from erpnext.assets.doctype.asset.depreciation import (
get_gl_entries_on_asset_disposal,
get_gl_entries_on_asset_regain,
reset_depreciation_schedule,
- reverse_depreciation_entry_made_after_disposal,
+ reverse_depreciation_entry_made_on_disposal,
)
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
from erpnext.controllers.accounts_controller import validate_account_head
@@ -368,21 +368,34 @@ class SalesInvoice(SellingController):
validate_docs_for_deferred_accounting([self.name], [])
def validate_fixed_asset(self):
- for d in self.get("items"):
- if d.is_fixed_asset and d.meta.get_field("asset") and d.asset:
- asset = frappe.get_doc("Asset", d.asset)
- if self.doctype == "Sales Invoice" and self.docstatus == 1:
- if self.update_stock:
- frappe.throw(_("'Update Stock' cannot be checked for fixed asset sale"))
+ if self.doctype != "Sales Invoice":
+ return
- elif asset.status in ("Scrapped", "Cancelled", "Capitalized") or (
- asset.status == "Sold" and not self.is_return
- ):
- frappe.throw(
- _("Row #{0}: Asset {1} cannot be submitted, it is already {2}").format(
- d.idx, d.asset, asset.status
+ for d in self.get("items"):
+ if d.is_fixed_asset:
+ if d.asset:
+ if not self.is_return:
+ asset_status = frappe.db.get_value("Asset", d.asset, "status")
+ if self.update_stock:
+ frappe.throw(_("'Update Stock' cannot be checked for fixed asset sale"))
+
+ elif asset_status in ("Scrapped", "Cancelled", "Capitalized"):
+ frappe.throw(
+ _("Row #{0}: Asset {1} cannot be sold, it is already {2}").format(
+ d.idx, d.asset, asset_status
+ )
)
+ elif asset_status == "Sold" and not self.is_return:
+ frappe.throw(_("Row #{0}: Asset {1} is already sold").format(d.idx, d.asset))
+ elif not self.return_against:
+ frappe.throw(
+ _("Row #{0}: Return Against is required for returning asset").format(d.idx)
)
+ else:
+ frappe.throw(
+ _("Row #{0}: You must select an Asset for Item {1}.").format(d.idx, d.item_code),
+ title=_("Missing Asset"),
+ )
def validate_item_cost_centers(self):
for item in self.items:
@@ -464,6 +477,8 @@ class SalesInvoice(SellingController):
self.update_stock_reservation_entries()
self.update_stock_ledger()
+ self.process_asset_depreciation()
+
# this sequence because outstanding may get -ve
self.make_gl_entries()
@@ -583,6 +598,8 @@ class SalesInvoice(SellingController):
if self.update_stock == 1:
self.update_stock_ledger()
+ self.process_asset_depreciation()
+
self.make_gl_entries_on_cancel()
if self.update_stock == 1:
@@ -1253,6 +1270,90 @@ class SalesInvoice(SellingController):
):
throw(_("Delivery Note {0} is not submitted").format(d.delivery_note))
+ def process_asset_depreciation(self):
+ if (self.is_return and self.docstatus == 2) or (not self.is_return and self.docstatus == 1):
+ self.depreciate_asset_on_sale()
+ else:
+ self.restore_asset()
+
+ self.update_asset()
+
+ def depreciate_asset_on_sale(self):
+ """
+ Depreciate asset on sale or cancellation of return sales invoice
+ """
+ disposal_date = self.get_disposal_date()
+ for d in self.get("items"):
+ if d.asset:
+ asset = frappe.get_doc("Asset", d.asset)
+ if asset.calculate_depreciation and asset.status != "Fully Depreciated":
+ depreciate_asset(asset, disposal_date, self.get_note_for_asset_sale(asset))
+
+ def get_note_for_asset_sale(self, asset):
+ return _("This schedule was created when Asset {0} was {1} through Sales Invoice {2}.").format(
+ get_link_to_form(asset.doctype, asset.name),
+ _("returned") if self.is_return else _("sold"),
+ get_link_to_form(self.doctype, self.get("name")),
+ )
+
+ def restore_asset(self):
+ """
+ Restore asset on return or cancellation of original sales invoice
+ """
+
+ for d in self.get("items"):
+ if d.asset:
+ asset = frappe.get_cached_doc("Asset", d.asset)
+ if asset.calculate_depreciation:
+ reverse_depreciation_entry_made_on_disposal(asset)
+
+ note = self.get_note_for_asset_return(asset)
+ reset_depreciation_schedule(asset, note)
+
+ def get_note_for_asset_return(self, asset):
+ asset_link = get_link_to_form(asset.doctype, asset.name)
+ invoice_link = get_link_to_form(self.doctype, self.get("name"))
+ if self.is_return:
+ return _(
+ "This schedule was created when Asset {0} was returned through Sales Invoice {1}."
+ ).format(asset_link, invoice_link)
+ else:
+ return _(
+ "This schedule was created when Asset {0} was restored due to Sales Invoice {1} cancellation."
+ ).format(asset_link, invoice_link)
+
+ def update_asset(self):
+ """
+ Update asset status, disposal date and asset activity on sale or return sales invoice
+ """
+
+ def _update_asset(asset, disposal_date, note, asset_status=None):
+ frappe.db.set_value("Asset", d.asset, "disposal_date", disposal_date)
+ add_asset_activity(asset.name, note)
+ asset.set_status(asset_status)
+
+ disposal_date = self.get_disposal_date()
+ for d in self.get("items"):
+ if d.asset:
+ asset = frappe.get_cached_doc("Asset", d.asset)
+
+ if (self.is_return and self.docstatus == 1) or (not self.is_return and self.docstatus == 2):
+ note = _("Asset returned") if self.is_return else _("Asset sold")
+ asset_status, disposal_date = None, None
+ else:
+ note = _("Asset sold") if not self.is_return else _("Return invoice of asset cancelled")
+ asset_status = "Sold"
+
+ _update_asset(asset, disposal_date, note, asset_status)
+
+ def get_disposal_date(self):
+ if self.is_return:
+ disposal_date = frappe.db.get_value("Sales Invoice", self.return_against, "posting_date")
+ else:
+ disposal_date = self.posting_date
+
+ return disposal_date
+
def make_gl_entries(self, gl_entries=None, from_repost=False):
from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries
@@ -1426,64 +1527,8 @@ class SalesInvoice(SellingController):
if self.is_internal_transfer():
continue
- if item.is_fixed_asset:
- asset = self.get_asset(item)
-
- if self.is_return:
- fixed_asset_gl_entries = get_gl_entries_on_asset_regain(
- asset,
- item.base_net_amount,
- item.finance_book,
- self.get("doctype"),
- self.get("name"),
- self.get("posting_date"),
- )
- asset.db_set("disposal_date", None)
- add_asset_activity(asset.name, _("Asset returned"))
-
- if asset.calculate_depreciation:
- posting_date = frappe.db.get_value(
- "Sales Invoice", self.return_against, "posting_date"
- )
- reverse_depreciation_entry_made_after_disposal(asset, posting_date)
- notes = _(
- "This schedule was created when Asset {0} was returned through Sales Invoice {1}."
- ).format(
- get_link_to_form(asset.doctype, asset.name),
- get_link_to_form(self.doctype, self.get("name")),
- )
- reset_depreciation_schedule(asset, self.posting_date, notes)
- asset.reload()
-
- else:
- if asset.calculate_depreciation:
- if not asset.status == "Fully Depreciated":
- notes = _(
- "This schedule was created when Asset {0} was sold through Sales Invoice {1}."
- ).format(
- get_link_to_form(asset.doctype, asset.name),
- get_link_to_form(self.doctype, self.get("name")),
- )
- depreciate_asset(asset, self.posting_date, notes)
- asset.reload()
-
- fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
- asset,
- item.base_net_amount,
- item.finance_book,
- self.get("doctype"),
- self.get("name"),
- self.get("posting_date"),
- )
- asset.db_set("disposal_date", self.posting_date)
- add_asset_activity(asset.name, _("Asset sold"))
-
- for gle in fixed_asset_gl_entries:
- gle["against"] = self.customer
- gl_entries.append(self.get_gl_dict(gle, item=item))
-
- self.set_asset_status(asset)
-
+ if item.is_fixed_asset and item.asset:
+ self.get_gl_entries_for_fixed_asset(item, gl_entries)
else:
income_account = (
item.income_account
@@ -1518,17 +1563,31 @@ class SalesInvoice(SellingController):
if cint(self.update_stock) and erpnext.is_perpetual_inventory_enabled(self.company):
gl_entries += super().get_gl_entries()
- def get_asset(self, item):
- if item.get("asset"):
- asset = frappe.get_doc("Asset", item.asset)
+ def get_gl_entries_for_fixed_asset(self, item, gl_entries):
+ asset = frappe.get_cached_doc("Asset", item.asset)
+
+ if self.is_return:
+ fixed_asset_gl_entries = get_gl_entries_on_asset_regain(
+ asset,
+ item.base_net_amount,
+ item.finance_book,
+ self.get("doctype"),
+ self.get("name"),
+ self.get("posting_date"),
+ )
else:
- frappe.throw(
- _("Row #{0}: You must select an Asset for Item {1}.").format(item.idx, item.item_name),
- title=_("Missing Asset"),
+ fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
+ asset,
+ item.base_net_amount,
+ item.finance_book,
+ self.get("doctype"),
+ self.get("name"),
+ self.get("posting_date"),
)
- self.check_finance_books(item, asset)
- return asset
+ for gle in fixed_asset_gl_entries:
+ gle["against"] = self.customer
+ gl_entries.append(self.get_gl_dict(gle, item=item))
@property
def enable_discount_accounting(self):
@@ -1539,12 +1598,6 @@ class SalesInvoice(SellingController):
return self._enable_discount_accounting
- def set_asset_status(self, asset):
- if self.is_return:
- asset.set_status()
- else:
- asset.set_status("Sold" if self.docstatus == 1 else None)
-
def make_loyalty_point_redemption_gle(self, gl_entries):
if cint(self.redeem_loyalty_points and self.loyalty_points and not self.is_consolidated):
gl_entries.append(
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 692ba62218b..2c422d9271a 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -2979,7 +2979,7 @@ class TestSalesInvoice(ERPNextTestSuite):
expected_values = [
["2020-06-30", 1366.12, 1366.12],
["2021-06-30", 20000.0, 21366.12],
- ["2021-09-30", 5041.1, 26407.22],
+ ["2021-09-30", 5041.34, 26407.46],
]
for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")):
@@ -3011,7 +3011,7 @@ class TestSalesInvoice(ERPNextTestSuite):
)
asset.load_from_db()
- expected_values = [["2020-12-31", 30000, 30000], ["2021-12-31", 30000, 60000]]
+ expected_values = [["2020-12-31", 30000, 30000], ["2021-12-31", 30041.15, 60041.15]]
for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")):
self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
@@ -3019,35 +3019,6 @@ class TestSalesInvoice(ERPNextTestSuite):
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
self.assertTrue(schedule.journal_entry)
- def test_depreciation_on_return_of_sold_asset(self):
- from erpnext.controllers.sales_and_purchase_return import make_return_doc
-
- create_asset_data()
- asset = create_asset(item_code="Macbook Pro", calculate_depreciation=1, submit=1)
- post_depreciation_entries(getdate("2021-09-30"))
-
- si = create_sales_invoice(
- item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000, posting_date=getdate("2021-09-30")
- )
- return_si = make_return_doc("Sales Invoice", si.name)
- return_si.submit()
- asset.load_from_db()
-
- expected_values = [
- ["2020-06-30", 1366.12, 1366.12, True],
- ["2021-06-30", 20000.0, 21366.12, True],
- ["2022-06-30", 20000.0, 41366.12, False],
- ["2023-06-30", 20000.0, 61366.12, False],
- ["2024-06-30", 20000.0, 81366.12, False],
- ["2025-06-06", 18633.88, 100000.0, False],
- ]
-
- for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")):
- self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
- self.assertEqual(expected_values[i][1], schedule.depreciation_amount)
- self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
- self.assertEqual(schedule.journal_entry, schedule.journal_entry)
-
def test_sales_invoice_against_supplier(self):
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
make_customer,
diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js
index 93c0fea0c4a..94b45bc08e6 100644
--- a/erpnext/assets/doctype/asset/asset.js
+++ b/erpnext/assets/doctype/asset/asset.js
@@ -103,14 +103,26 @@ frappe.ui.form.on("Asset", {
},
__("Manage")
);
- } else if (frm.doc.status == "Scrapped") {
+
frm.add_custom_button(
- __("Restore Asset"),
+ __("Repair Asset"),
function () {
- erpnext.asset.restore_asset(frm);
+ frm.trigger("create_asset_repair");
},
__("Manage")
);
+
+ frm.add_custom_button(
+ __("Split Asset"),
+ function () {
+ frm.trigger("split_asset");
+ },
+ __("Manage")
+ );
+ } else if (frm.doc.status == "Scrapped") {
+ frm.add_custom_button(__("Restore Asset"), function () {
+ erpnext.asset.restore_asset(frm);
+ }).addClass("btn-primary");
}
if (frm.doc.maintenance_required && !frm.doc.maintenance_schedule) {
@@ -123,23 +135,7 @@ frappe.ui.form.on("Asset", {
);
}
- frm.add_custom_button(
- __("Repair Asset"),
- function () {
- frm.trigger("create_asset_repair");
- },
- __("Manage")
- );
-
- frm.add_custom_button(
- __("Split Asset"),
- function () {
- frm.trigger("split_asset");
- },
- __("Manage")
- );
-
- if (frm.doc.status != "Fully Depreciated") {
+ if (["Submitted", "Partially Depreciated"].includes(frm.doc.status)) {
frm.add_custom_button(
__("Adjust Asset Value"),
function () {
diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json
index b18f122721b..dfc7949c544 100644
--- a/erpnext/assets/doctype/asset/asset.json
+++ b/erpnext/assets/doctype/asset/asset.json
@@ -405,7 +405,6 @@
"read_only": 1
},
{
- "collapsible": 1,
"collapsible_depends_on": "is_existing_asset",
"fieldname": "purchase_details_section",
"fieldtype": "Section Break",
@@ -429,6 +428,7 @@
"fieldname": "split_from",
"fieldtype": "Link",
"label": "Split From",
+ "no_copy": 1,
"options": "Asset",
"read_only": 1
},
@@ -483,6 +483,7 @@
"read_only": 1
},
{
+ "default": "0",
"depends_on": "eval:doc.docstatus > 0",
"fieldname": "additional_asset_cost",
"fieldtype": "Currency",
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index 4fd135d359a..9f57e4165fb 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -33,9 +33,6 @@ from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_sched
convert_draft_asset_depr_schedules_into_active,
get_asset_depr_schedule_doc,
get_depr_schedule,
- make_draft_asset_depr_schedules,
- make_draft_asset_depr_schedules_if_not_present,
- update_draft_asset_depr_schedules,
)
from erpnext.controllers.accounts_controller import AccountsController
@@ -131,29 +128,60 @@ class Asset(AccountsController):
self.set_missing_values()
self.validate_gross_and_purchase_amount()
self.validate_finance_books()
+ self.total_asset_cost = self.gross_purchase_amount + self.additional_asset_cost
+ self.status = self.get_status()
- if not self.split_from:
- self.prepare_depreciation_data()
+ def create_asset_depreciation_schedule(self):
+ self.set_depr_rate_and_value_after_depreciation()
- if self.calculate_depreciation:
- update_draft_asset_depr_schedules(self)
+ if self.split_from or not self.calculate_depreciation:
+ return
- if frappe.db.exists("Asset", self.name):
- asset_depr_schedules_names = make_draft_asset_depr_schedules_if_not_present(self)
+ schedules = []
+ for row in self.get("finance_books"):
+ self.validate_asset_finance_books(row)
+ if not row.rate_of_depreciation:
+ row.rate_of_depreciation = self.get_depreciation_rate(row, on_validate=True)
- if asset_depr_schedules_names:
- asset_depr_schedules_links = get_comma_separated_links(
- asset_depr_schedules_names, "Asset Depreciation Schedule"
- )
- frappe.msgprint(
- _(
- "Asset Depreciation Schedules created:
{0}
Please check, edit if needed, and submit the Asset."
- ).format(asset_depr_schedules_links)
- )
+ schedule_doc = get_asset_depr_schedule_doc(self.name, "Draft", row.finance_book)
+ if not schedule_doc:
+ schedule_doc = frappe.new_doc("Asset Depreciation Schedule")
+ schedule_doc.asset = self.name
+ schedule_doc.create_depreciation_schedule(row)
+ schedule_doc.save()
+ schedules.append(schedule_doc.name)
+
+ self.show_schedule_creation_message(schedules)
+
+ def set_depr_rate_and_value_after_depreciation(self):
+ if self.split_from:
+ return
+
+ self.value_after_depreciation = (
+ flt(self.gross_purchase_amount)
+ - flt(self.opening_accumulated_depreciation)
+ + flt(self.additional_asset_cost)
+ )
+ if self.calculate_depreciation:
+ self.set_depreciation_rate()
+ for d in self.finance_books:
+ d.db_set("value_after_depreciation", self.value_after_depreciation)
+ else:
+ self.finance_books = []
+
+ def show_schedule_creation_message(self, schedules):
+ if schedules:
+ asset_depr_schedules_links = get_comma_separated_links(schedules, "Asset Depreciation Schedule")
+ frappe.msgprint(
+ _(
+ "Asset Depreciation Schedules created/updated:
{0}
Please check, edit if needed, and submit the Asset."
+ ).format(asset_depr_schedules_links)
+ )
+
+ def on_update(self):
+ self.create_asset_depreciation_schedule()
self.validate_expected_value_after_useful_life()
self.set_total_booked_depreciations()
- self.total_asset_cost = self.gross_purchase_amount
- self.status = self.get_status()
def on_submit(self):
self.validate_in_use_date()
@@ -179,16 +207,6 @@ class Asset(AccountsController):
add_asset_activity(self.name, _("Asset cancelled"))
def after_insert(self):
- if self.calculate_depreciation and not self.split_from:
- asset_depr_schedules_names = make_draft_asset_depr_schedules(self)
- asset_depr_schedules_links = get_comma_separated_links(
- asset_depr_schedules_names, "Asset Depreciation Schedule"
- )
- frappe.msgprint(
- _(
- "Asset Depreciation Schedules created:
{0}
Please check, edit if needed, and submit the Asset."
- ).format(asset_depr_schedules_links)
- )
if (
not frappe.db.exists(
{
@@ -250,16 +268,6 @@ class Asset(AccountsController):
if self.is_existing_asset and self.purchase_invoice:
frappe.throw(_("Purchase Invoice cannot be made against an existing asset {0}").format(self.name))
- def prepare_depreciation_data(self):
- if self.calculate_depreciation:
- self.value_after_depreciation = 0
- self.set_depreciation_rate()
- else:
- self.finance_books = []
- self.value_after_depreciation = flt(self.gross_purchase_amount) - flt(
- self.opening_accumulated_depreciation
- )
-
def validate_item(self):
item = frappe.get_cached_value(
"Item", self.item_code, ["is_fixed_asset", "is_stock_item", "disabled"], as_dict=1
@@ -456,61 +464,65 @@ class Asset(AccountsController):
frappe.throw(
_("Row {0}: Expected Value After Useful Life must be less than Gross Purchase Amount").format(
row.idx
- ),
- title=_("Invalid Schedule"),
+ )
)
if not row.depreciation_start_date:
- if not self.available_for_use_date:
- frappe.throw(
- _("Row {0}: Depreciation Start Date is required").format(row.idx),
- title=_("Invalid Schedule"),
- )
row.depreciation_start_date = get_last_day(self.available_for_use_date)
+ self.validate_depreciation_start_date(row)
if not self.is_existing_asset:
self.opening_accumulated_depreciation = 0
self.opening_number_of_booked_depreciations = 0
else:
- depreciable_amount = flt(
- flt(self.gross_purchase_amount) - flt(row.expected_value_after_useful_life),
- self.precision("gross_purchase_amount"),
- )
- if flt(self.opening_accumulated_depreciation) > depreciable_amount:
- frappe.throw(
- _("Opening Accumulated Depreciation must be less than or equal to {0}").format(
- depreciable_amount
- )
- )
+ self.validate_opening_depreciation_values(row)
- if self.opening_accumulated_depreciation:
- if not self.opening_number_of_booked_depreciations:
- frappe.throw(_("Please set Opening Number of Booked Depreciations"))
- else:
- self.opening_number_of_booked_depreciations = 0
-
- if flt(row.total_number_of_depreciations) <= cint(self.opening_number_of_booked_depreciations):
- frappe.throw(
- _(
- "Row {0}: Total Number of Depreciations cannot be less than or equal to Opening Number of Booked Depreciations"
- ).format(row.idx),
- title=_("Invalid Schedule"),
- )
-
- if row.depreciation_start_date and getdate(row.depreciation_start_date) < getdate(self.purchase_date):
+ def validate_opening_depreciation_values(self, row):
+ row.expected_value_after_useful_life = flt(
+ row.expected_value_after_useful_life, self.precision("gross_purchase_amount")
+ )
+ depreciable_amount = flt(
+ flt(self.gross_purchase_amount) - flt(row.expected_value_after_useful_life),
+ self.precision("gross_purchase_amount"),
+ )
+ if flt(self.opening_accumulated_depreciation) > depreciable_amount:
frappe.throw(
- _("Depreciation Row {0}: Next Depreciation Date cannot be before Purchase Date").format(
- row.idx
+ _("Row #{0}: Opening Accumulated Depreciation must be less than or equal to {1}").format(
+ row.idx, depreciable_amount
)
)
- if row.depreciation_start_date and getdate(row.depreciation_start_date) < getdate(
- self.available_for_use_date
- ):
+ if self.opening_accumulated_depreciation:
+ if not self.opening_number_of_booked_depreciations:
+ frappe.throw(_("Please set opening number of booked depreciations"))
+ else:
+ self.opening_number_of_booked_depreciations = 0
+
+ if flt(row.total_number_of_depreciations) <= cint(self.opening_number_of_booked_depreciations):
frappe.throw(
_(
- "Depreciation Row {0}: Next Depreciation Date cannot be before Available-for-use Date"
- ).format(row.idx)
+ "Row #{0}: Total Number of Depreciations cannot be less than or equal to Opening Number of Booked Depreciations"
+ ).format(row.idx),
+ title=_("Invalid Schedule"),
+ )
+
+ def validate_depreciation_start_date(self, row):
+ if row.depreciation_start_date:
+ if getdate(row.depreciation_start_date) < getdate(self.purchase_date):
+ frappe.throw(
+ _("Row #{0}: Next Depreciation Date cannot be before Purchase Date").format(row.idx)
+ )
+
+ if getdate(row.depreciation_start_date) < getdate(self.available_for_use_date):
+ frappe.throw(
+ _("Row #{0}: Next Depreciation Date cannot be before Available-for-use Date").format(
+ row.idx
+ )
+ )
+ else:
+ frappe.throw(
+ _("Row #{0}: Depreciation Start Date is required").format(row.idx),
+ title=_("Invalid Schedule"),
)
def set_total_booked_depreciations(self):
@@ -530,15 +542,11 @@ class Asset(AccountsController):
if not depr_schedule:
continue
- accumulated_depreciation_after_full_schedule = [
- d.accumulated_depreciation_amount for d in depr_schedule
- ]
+ accumulated_depreciation_after_full_schedule = max(
+ [d.accumulated_depreciation_amount for d in depr_schedule]
+ )
if accumulated_depreciation_after_full_schedule:
- accumulated_depreciation_after_full_schedule = max(
- accumulated_depreciation_after_full_schedule
- )
-
asset_value_after_full_schedule = flt(
flt(self.gross_purchase_amount) - flt(accumulated_depreciation_after_full_schedule),
self.precision("gross_purchase_amount"),
@@ -607,7 +615,10 @@ class Asset(AccountsController):
def get_status(self):
"""Returns status based on whether it is draft, submitted, scrapped or depreciated"""
if self.docstatus == 0:
- status = "Draft"
+ if self.is_composite_asset:
+ status = "Work In Progress"
+ else:
+ status = "Draft"
elif self.docstatus == 1:
status = "Submitted"
@@ -624,13 +635,13 @@ class Asset(AccountsController):
].expected_value_after_useful_life
value_after_depreciation = self.finance_books[idx].value_after_depreciation
- if (
- flt(value_after_depreciation) <= expected_value_after_useful_life
- or self.is_fully_depreciated
- ):
- status = "Fully Depreciated"
- elif flt(value_after_depreciation) < flt(self.gross_purchase_amount):
- status = "Partially Depreciated"
+ if (
+ flt(value_after_depreciation) <= expected_value_after_useful_life
+ or self.is_fully_depreciated
+ ):
+ status = "Fully Depreciated"
+ elif flt(value_after_depreciation) < flt(self.gross_purchase_amount):
+ status = "Partially Depreciated"
elif self.docstatus == 2:
status = "Cancelled"
return status
@@ -820,53 +831,52 @@ class Asset(AccountsController):
if isinstance(args, str):
args = json.loads(args)
- float_precision = cint(frappe.db.get_default("float_precision")) or 2
+ rate_field_precision = frappe.get_precision(args.doctype, "rate_of_depreciation") or 2
if args.get("depreciation_method") == "Double Declining Balance":
- return 200.0 / (
+ return self.get_double_declining_balance_rate(args, rate_field_precision)
+ elif args.get("depreciation_method") == "Written Down Value":
+ return self.get_written_down_value_rate(args, rate_field_precision, on_validate)
+
+ def get_double_declining_balance_rate(self, args, rate_field_precision):
+ return flt(
+ 200.0
+ / (
(
flt(args.get("total_number_of_depreciations"), 2)
* flt(args.get("frequency_of_depreciation"))
)
/ 12
- )
+ ),
+ rate_field_precision,
+ )
- if args.get("depreciation_method") == "Written Down Value":
- if (
- args.get("rate_of_depreciation")
- and on_validate
- and not self.flags.increase_in_asset_value_due_to_repair
- ):
- return args.get("rate_of_depreciation")
+ def get_written_down_value_rate(self, args, rate_field_precision, on_validate):
+ if args.get("rate_of_depreciation") and on_validate:
+ return args.get("rate_of_depreciation")
- if args.get("rate_of_depreciation") and not flt(args.get("expected_value_after_useful_life")):
- return args.get("rate_of_depreciation")
+ if args.get("rate_of_depreciation") and not flt(args.get("expected_value_after_useful_life")):
+ return args.get("rate_of_depreciation")
- if self.flags.increase_in_asset_value_due_to_repair:
- value = flt(args.get("expected_value_after_useful_life")) / flt(
- args.get("value_after_depreciation")
- )
- else:
- value = flt(args.get("expected_value_after_useful_life")) / (
- flt(self.gross_purchase_amount) - flt(self.opening_accumulated_depreciation)
- )
+ if flt(args.get("value_after_depreciation")):
+ current_asset_value = flt(args.get("value_after_depreciation"))
+ else:
+ current_asset_value = flt(self.gross_purchase_amount) - flt(self.opening_accumulated_depreciation)
- depreciation_rate = math.pow(
- value,
- 1.0
- / (
- (
- (
- flt(args.get("total_number_of_depreciations"), 2)
- - flt(self.opening_number_of_booked_depreciations)
- )
- * flt(args.get("frequency_of_depreciation"))
- )
- / 12
- ),
- )
+ value = flt(args.get("expected_value_after_useful_life")) / current_asset_value
- return flt((100 * (1 - depreciation_rate)), float_precision)
+ pending_number_of_depreciations = (
+ flt(args.get("total_number_of_depreciations"), 2)
+ - flt(self.opening_number_of_booked_depreciations)
+ - flt(args.get("total_number_of_booked_depreciations"))
+ )
+ pending_years = (
+ pending_number_of_depreciations * flt(args.get("frequency_of_depreciation"))
+ + cint(args.get("increase_in_asset_life"))
+ ) / 12
+
+ depreciation_rate = 100 * (1 - math.pow(value, 1.0 / pending_years))
+ return flt(depreciation_rate, rate_field_precision)
def has_gl_entries(doctype, docname, target_account):
@@ -923,7 +933,7 @@ def get_asset_naming_series():
@frappe.whitelist()
-def make_sales_invoice(asset, item_code, company, serial_no=None):
+def make_sales_invoice(asset, item_code, company, serial_no=None, posting_date=None):
asset_doc = frappe.get_doc("Asset", asset)
si = frappe.new_doc("Sales Invoice")
si.company = company
@@ -1185,166 +1195,203 @@ def get_values_from_purchase_doc(purchase_doc_name, item_code, doctype):
@frappe.whitelist()
def split_asset(asset_name, split_qty):
- asset = frappe.get_doc("Asset", asset_name)
+ """Split an asset into two based on the given quantity."""
+ existing_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"))
+ validate_split_quantity(existing_asset, split_qty)
+ remaining_qty = existing_asset.asset_quantity - split_qty
- remaining_qty = asset.asset_quantity - split_qty
+ # Create new asset and update existing one
+ splitted_asset = create_new_asset_from_split(existing_asset, split_qty)
+ update_existing_asset_after_split(existing_asset, remaining_qty, splitted_asset)
- new_asset = create_new_asset_after_split(asset, split_qty)
- update_existing_asset(asset, remaining_qty, new_asset.name)
+ return splitted_asset
+
+def validate_split_quantity(existing_asset, split_qty):
+ if split_qty >= existing_asset.asset_quantity:
+ frappe.throw(_("Split Quantity must be less than Asset Quantity"))
+
+
+def create_new_asset_from_split(existing_asset, split_qty):
+ """Create a new asset from the split quantity."""
+ return process_asset_split(existing_asset, split_qty, is_new_asset=True)
+
+
+def update_existing_asset_after_split(existing_asset, remaining_qty, splitted_asset):
+ """Update the existing asset with the remaining quantity."""
+ process_asset_split(existing_asset, remaining_qty, splitted_asset=splitted_asset)
+
+
+def process_asset_split(existing_asset, split_qty, splitted_asset=None, is_new_asset=False):
+ """Handle asset creation or update during the split."""
+ scaling_factor = flt(split_qty) / flt(existing_asset.asset_quantity)
+ new_asset = frappe.copy_doc(existing_asset) if is_new_asset else splitted_asset
+ asset_doc = new_asset if is_new_asset else existing_asset
+
+ set_split_asset_values(asset_doc, scaling_factor, split_qty, existing_asset, is_new_asset)
+ log_asset_activity(existing_asset, asset_doc, splitted_asset, is_new_asset)
+
+ # Update finance books and depreciation schedules
+ update_finance_books(asset_doc, existing_asset, new_asset, scaling_factor, is_new_asset)
return new_asset
-def update_existing_asset(asset, remaining_qty, new_asset_name):
- 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
+def set_split_asset_values(asset_doc, scaling_factor, split_qty, existing_asset, is_new_asset):
+ asset_doc.gross_purchase_amount = existing_asset.gross_purchase_amount * scaling_factor
+ asset_doc.purchase_amount = existing_asset.gross_purchase_amount
+ asset_doc.additional_asset_cost = existing_asset.additional_asset_cost * scaling_factor
+ asset_doc.total_asset_cost = asset_doc.gross_purchase_amount + asset_doc.additional_asset_cost
+ asset_doc.opening_accumulated_depreciation = (
+ existing_asset.opening_accumulated_depreciation * scaling_factor
)
+ asset_doc.value_after_depreciation = existing_asset.value_after_depreciation * scaling_factor
+ asset_doc.asset_quantity = split_qty
+ asset_doc.split_from = existing_asset.name if is_new_asset else None
- 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 row in asset_doc.get("finance_books"):
+ row.value_after_depreciation = row.value_after_depreciation * scaling_factor
+ row.expected_value_after_useful_life = row.expected_value_after_useful_life * scaling_factor
- add_asset_activity(
- asset.name,
- _("Asset updated after being split into Asset {0}").format(get_link_to_form("Asset", new_asset_name)),
- )
+ if not is_new_asset:
+ asset_doc.flags.ignore_validate_update_after_submit = True
+ asset_doc.save()
- for row in asset.get("finance_books"):
- value_after_depreciation = flt((row.value_after_depreciation * remaining_qty) / asset.asset_quantity)
- expected_value_after_useful_life = flt(
- (row.expected_value_after_useful_life * remaining_qty) / asset.asset_quantity
+
+def log_asset_activity(existing_asset, asset_doc, splitted_asset, is_new_asset):
+ if is_new_asset:
+ asset_doc.insert()
+ add_asset_activity(
+ asset_doc.name,
+ _("Asset created after being split from Asset {0}").format(
+ get_link_to_form("Asset", existing_asset.name)
+ ),
)
- frappe.db.set_value(
- "Asset Finance Book", row.name, "value_after_depreciation", value_after_depreciation
- )
- frappe.db.set_value(
- "Asset Finance Book",
- row.name,
- "expected_value_after_useful_life",
- expected_value_after_useful_life,
+ asset_doc.submit()
+ asset_doc.set_status()
+ else:
+ add_asset_activity(
+ existing_asset.name,
+ _("Asset updated after being split into Asset {0}").format(
+ get_link_to_form("Asset", splitted_asset.name)
+ ),
)
- current_asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active", row.finance_book)
- new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc)
- new_asset_depr_schedule_doc.set_draft_asset_depr_schedule_details(asset, row)
+def update_finance_books(asset_doc, existing_asset, new_asset, scaling_factor, is_new_asset):
+ """Update finance books and depreciation schedules for the asset."""
+ for fb_row in asset_doc.get("finance_books"):
+ reschedule_depr_for_updated_asset(existing_asset, new_asset, fb_row, scaling_factor, is_new_asset)
- accumulated_depreciation = 0
-
- for term in new_asset_depr_schedule_doc.get("depreciation_schedule"):
- depreciation_amount = flt((term.depreciation_amount * remaining_qty) / asset.asset_quantity)
- term.depreciation_amount = depreciation_amount
- accumulated_depreciation += depreciation_amount
- term.accumulated_depreciation_amount = accumulated_depreciation
-
- notes = _(
- "This schedule was created when Asset {0} was updated after being split into new Asset {1}."
- ).format(get_link_to_form(asset.doctype, asset.name), get_link_to_form(asset.doctype, new_asset_name))
- new_asset_depr_schedule_doc.notes = notes
-
- current_asset_depr_schedule_doc.flags.should_not_cancel_depreciation_entries = True
- current_asset_depr_schedule_doc.cancel()
-
- new_asset_depr_schedule_doc.submit()
+ # Add references in journal entries for new asset
+ if is_new_asset:
+ for row in new_asset.get("finance_books"):
+ depr_schedule_doc = get_depr_schedule(new_asset.name, "Active", row.finance_book)
+ for schedule in depr_schedule_doc:
+ if schedule.journal_entry:
+ add_reference_in_jv_on_split(
+ schedule.journal_entry,
+ new_asset.name,
+ existing_asset.name,
+ schedule.depreciation_amount,
+ )
-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
+def reschedule_depr_for_updated_asset(existing_asset, new_asset, fb_row, scaling_factor, is_new_asset):
+ """Reschedule depreciation for an asset after a split."""
+ current_depr_schedule_doc = get_asset_depr_schedule_doc(
+ existing_asset.name, "Active", fb_row.finance_book
+ )
+ if not current_depr_schedule_doc:
+ return
+
+ # Create a new depreciation schedule based on the current one
+ new_depr_schedule_doc = create_new_depr_schedule(
+ current_depr_schedule_doc, existing_asset, new_asset, is_new_asset, fb_row
)
- new_asset.gross_purchase_amount = new_gross_purchase_amount
- if asset.purchase_amount:
- new_asset.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
+ update_depreciation_terms(new_depr_schedule_doc, scaling_factor)
+ add_depr_schedule_notes(new_depr_schedule_doc, existing_asset, new_asset, is_new_asset)
- for row in new_asset.get("finance_books"):
- row.value_after_depreciation = flt((row.value_after_depreciation * split_qty) / asset.asset_quantity)
- row.expected_value_after_useful_life = flt(
- (row.expected_value_after_useful_life * split_qty) / asset.asset_quantity
+ if not is_new_asset:
+ current_depr_schedule_doc.flags.should_not_cancel_depreciation_entries = True
+ current_depr_schedule_doc.cancel()
+
+ new_depr_schedule_doc.submit()
+
+
+def create_new_depr_schedule(current_depr_schedule_doc, existing_asset, new_asset, is_new_asset, fb_row):
+ """Create a new depreciation schedule based on the current one."""
+ new_depr_schedule_doc = frappe.copy_doc(current_depr_schedule_doc)
+ new_depr_schedule_doc.asset_doc = new_asset if is_new_asset else existing_asset
+ new_depr_schedule_doc.fb_row = fb_row
+ new_depr_schedule_doc.fetch_asset_details()
+ return new_depr_schedule_doc
+
+
+def update_depreciation_terms(new_depr_schedule_doc, scaling_factor):
+ """Update depreciation terms with scaled amounts."""
+ accumulated_depreciation = 0
+ for term in new_depr_schedule_doc.get("depreciation_schedule"):
+ depreciation_amount = flt(
+ term.depreciation_amount * scaling_factor, term.precision("depreciation_amount")
)
+ term.depreciation_amount = depreciation_amount
+ accumulated_depreciation = flt(
+ accumulated_depreciation + depreciation_amount, term.precision("depreciation_amount")
+ )
+ term.accumulated_depreciation_amount = accumulated_depreciation
- new_asset.insert()
- add_asset_activity(
- new_asset.name,
- _("Asset created after being split from Asset {0}").format(get_link_to_form("Asset", asset.name)),
+def add_depr_schedule_notes(new_depr_schedule_doc, existing_asset, new_asset, is_new_asset):
+ notes = _("This schedule was created when Asset {0} was {1} into new Asset {2}.").format(
+ get_link_to_form(existing_asset.doctype, existing_asset.name),
+ "split" if is_new_asset else "updated after being split",
+ get_link_to_form(new_asset.doctype, new_asset.name),
)
-
- new_asset.submit()
- new_asset.set_status()
-
- for row in new_asset.get("finance_books"):
- current_asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active", row.finance_book)
- if not current_asset_depr_schedule_doc:
- continue
- new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc)
-
- new_asset_depr_schedule_doc.set_draft_asset_depr_schedule_details(new_asset, row)
-
- accumulated_depreciation = 0
-
- for term in new_asset_depr_schedule_doc.get("depreciation_schedule"):
- depreciation_amount = flt((term.depreciation_amount * split_qty) / asset.asset_quantity)
- term.depreciation_amount = depreciation_amount
- accumulated_depreciation += depreciation_amount
- term.accumulated_depreciation_amount = accumulated_depreciation
-
- notes = _("This schedule was created when new Asset {0} was split from Asset {1}.").format(
- get_link_to_form(new_asset.doctype, new_asset.name), get_link_to_form(asset.doctype, asset.name)
- )
- new_asset_depr_schedule_doc.notes = notes
-
- new_asset_depr_schedule_doc.submit()
-
- for row in new_asset.get("finance_books"):
- depr_schedule = get_depr_schedule(new_asset.name, "Active", row.finance_book)
- for term in depr_schedule:
- # Update references in JV
- if term.journal_entry:
- add_reference_in_jv_on_split(
- term.journal_entry, new_asset.name, asset.name, term.depreciation_amount
- )
-
- return new_asset
+ new_depr_schedule_doc.notes = notes
def add_reference_in_jv_on_split(entry_name, new_asset_name, old_asset_name, depreciation_amount):
+ """Add a reference to a new asset in a journal entry after a split."""
journal_entry = frappe.get_doc("Journal Entry", entry_name)
entries_to_add = []
- idx = len(journal_entry.get("accounts")) + 1
+ adjust_existing_accounts(journal_entry, old_asset_name, depreciation_amount, entries_to_add)
+ add_new_entries(journal_entry, entries_to_add, new_asset_name, depreciation_amount)
+
+ # Save and repost the journal entry
+ journal_entry.flags.ignore_validate_update_after_submit = True
+ journal_entry.save()
+
+ journal_entry.docstatus = 2
+ journal_entry.make_gl_entries(1)
+ journal_entry.docstatus = 1
+ journal_entry.make_gl_entries()
+
+
+def adjust_existing_accounts(journal_entry, old_asset_name, depreciation_amount, entries_to_add):
+ """Adjust existing accounts and prepare new entries for the new asset."""
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
- )
+ adjust_account_balance(account, depreciation_amount)
+
+def adjust_account_balance(account, depreciation_amount):
+ """Adjust the balance of an account based on the depreciation amount."""
+ if account.credit:
+ account.credit -= depreciation_amount
+ account.credit_in_account_currency -= account.exchange_rate * depreciation_amount
+ elif account.debit:
+ account.debit -= depreciation_amount
+ account.debit_in_account_currency -= account.exchange_rate * depreciation_amount
+
+
+def add_new_entries(journal_entry, entries_to_add, new_asset_name, depreciation_amount):
+ """Add new entries for the new asset to the journal entry."""
+ idx = len(journal_entry.get("accounts")) + 1
for entry in entries_to_add:
entry.reference_name = new_asset_name
if entry.credit:
@@ -1353,17 +1400,6 @@ def add_reference_in_jv_on_split(entry_name, new_asset_name, old_asset_name, dep
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()
diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py
index 979c49a93eb..6eded8c9a4e 100644
--- a/erpnext/assets/doctype/asset/depreciation.py
+++ b/erpnext/assets/doctype/asset/depreciation.py
@@ -28,8 +28,8 @@ from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activ
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_asset_depr_schedule_doc,
get_asset_depr_schedule_name,
- get_temp_asset_depr_schedule_doc,
- make_new_active_asset_depr_schedules_and_cancel_current_ones,
+ get_temp_depr_schedule_doc,
+ reschedule_depreciation,
)
@@ -40,72 +40,43 @@ def post_depreciation_entries(date=None):
):
return
- if not date:
- date = today()
+ date = date or today()
+ book_depreciation_entries(date)
- failed_asset_names = []
- error_log_names = []
- depreciable_asset_depr_schedules_data = get_depreciable_asset_depr_schedules_data(date)
-
- credit_and_debit_accounts_for_asset_category_and_company = {}
- depreciation_cost_center_and_depreciation_series_for_company = (
- get_depreciation_cost_center_and_depreciation_series_for_company()
- )
+def book_depreciation_entries(date):
+ # Process depreciation entries for all depreciable assets
+ failed_assets, error_logs = [], []
+ depreciable_assets_data = get_depreciable_assets_data(date)
accounting_dimensions = get_checks_for_pl_and_bs_accounts()
- for asset_depr_schedule_data in depreciable_asset_depr_schedules_data:
- (
- asset_depr_schedule_name,
- asset_name,
- asset_category,
- asset_company,
- sch_start_idx,
- sch_end_idx,
- ) = asset_depr_schedule_data
-
- if (
- asset_category,
- asset_company,
- ) not in credit_and_debit_accounts_for_asset_category_and_company:
- credit_and_debit_accounts_for_asset_category_and_company.update(
- {
- (
- asset_category,
- asset_company,
- ): get_credit_and_debit_accounts_for_asset_category_and_company(
- asset_category, asset_company
- ),
- }
- )
+ for data in depreciable_assets_data:
+ (depr_schedule_name, asset_name, sch_start_idx, sch_end_idx) = data
try:
make_depreciation_entry(
- asset_depr_schedule_name,
+ depr_schedule_name,
date,
sch_start_idx,
sch_end_idx,
- credit_and_debit_accounts_for_asset_category_and_company[(asset_category, asset_company)],
- depreciation_cost_center_and_depreciation_series_for_company[asset_company],
accounting_dimensions,
)
frappe.db.commit()
except Exception as e:
frappe.db.rollback()
- failed_asset_names.append(asset_name)
+ failed_assets.append(asset_name)
error_log = frappe.log_error(e)
- error_log_names.append(error_log.name)
-
- if failed_asset_names:
- set_depr_entry_posting_status_for_failed_assets(failed_asset_names)
- notify_depr_entry_posting_error(failed_asset_names, error_log_names)
+ error_logs.append(error_log.name)
+ if failed_assets:
+ set_depr_entry_posting_status_for_failed_assets(failed_assets)
+ notify_depr_entry_posting_error(failed_assets, error_logs)
frappe.db.commit()
-def get_depreciable_asset_depr_schedules_data(date):
+def get_depreciable_assets_data(date):
a = frappe.qb.DocType("Asset")
ads = frappe.qb.DocType("Asset Depreciation Schedule")
ds = frappe.qb.DocType("Depreciation Schedule")
@@ -116,7 +87,7 @@ def get_depreciable_asset_depr_schedules_data(date):
.on(ads.asset == a.name)
.join(ds)
.on(ads.name == ds.parent)
- .select(ads.name, a.name, a.asset_category, a.company, Min(ds.idx) - 1, Max(ds.idx))
+ .select(ads.name, a.name, Min(ds.idx) - 1, Max(ds.idx))
.where(a.calculate_depreciation == 1)
.where(a.docstatus == 1)
.where(ads.docstatus == 1)
@@ -136,10 +107,10 @@ def get_depreciable_asset_depr_schedules_data(date):
return res
-def make_depreciation_entry_for_all_asset_depr_schedules(asset_doc, date=None):
+def make_depreciation_entry_on_disposal(asset_doc, disposal_date=None):
for row in asset_doc.get("finance_books"):
- asset_depr_schedule_name = get_asset_depr_schedule_name(asset_doc.name, "Active", row.finance_book)
- make_depreciation_entry(asset_depr_schedule_name, date)
+ depr_schedule_name = get_asset_depr_schedule_name(asset_doc.name, "Active", row.finance_book)
+ make_depreciation_entry(depr_schedule_name, disposal_date)
def get_acc_frozen_upto():
@@ -156,21 +127,26 @@ def get_acc_frozen_upto():
return
-def get_credit_and_debit_accounts_for_asset_category_and_company(asset_category, company):
- (
- _,
- accumulated_depreciation_account,
- depreciation_expense_account,
- ) = get_depreciation_accounts(asset_category, company)
+def get_credit_debit_accounts_for_asset(asset_category, company):
+ # Returns credit and debit accounts for the given asset category and company.
+ (_, accumulated_depr_account, depr_expense_account) = get_depreciation_accounts(asset_category, company)
credit_account, debit_account = get_credit_and_debit_accounts(
- accumulated_depreciation_account, depreciation_expense_account
+ accumulated_depr_account, depr_expense_account
)
return (credit_account, debit_account)
-def get_depreciation_cost_center_and_depreciation_series_for_company():
+def get_depreciation_cost_center_and_series(asset):
+ depreciation_cost_center, depreciation_series = frappe.get_cached_value(
+ "Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"]
+ )
+ depreciation_cost_center = asset.cost_center or depreciation_cost_center
+ return depreciation_cost_center, depreciation_series
+
+
+def get_depr_cost_center_and_series():
company_names = frappe.db.get_all("Company", pluck="name")
res = {}
@@ -179,89 +155,70 @@ def get_depreciation_cost_center_and_depreciation_series_for_company():
depreciation_cost_center, depreciation_series = frappe.get_cached_value(
"Company", company_name, ["depreciation_cost_center", "series_for_depreciation_entry"]
)
- res.update({company_name: (depreciation_cost_center, depreciation_series)})
+ res.setdefault(company_name, (depreciation_cost_center, depreciation_series))
return res
@frappe.whitelist()
def make_depreciation_entry(
- asset_depr_schedule_name,
+ depr_schedule_name,
date=None,
sch_start_idx=None,
sch_end_idx=None,
- credit_and_debit_accounts=None,
- depreciation_cost_center_and_depreciation_series=None,
accounting_dimensions=None,
):
frappe.has_permission("Journal Entry", throw=True)
+ date = date or today()
- if not date:
- date = today()
+ depr_schedule_doc = frappe.get_doc("Asset Depreciation Schedule", depr_schedule_name)
+ asset = frappe.get_doc("Asset", depr_schedule_doc.asset)
- asset_depr_schedule_doc = frappe.get_doc("Asset Depreciation Schedule", asset_depr_schedule_name)
+ credit_account, debit_account = get_credit_debit_accounts_for_asset(asset.asset_category, asset.company)
+ depr_cost_center, depr_series = get_depreciation_cost_center_and_series(asset)
+ accounting_dimensions = accounting_dimensions or get_checks_for_pl_and_bs_accounts()
+ depr_posting_error = None
- asset = frappe.get_doc("Asset", asset_depr_schedule_doc.asset)
-
- if credit_and_debit_accounts:
- credit_account, debit_account = credit_and_debit_accounts
- else:
- credit_account, debit_account = get_credit_and_debit_accounts_for_asset_category_and_company(
- asset.asset_category, asset.company
- )
-
- if depreciation_cost_center_and_depreciation_series:
- depreciation_cost_center, depreciation_series = depreciation_cost_center_and_depreciation_series
- else:
- depreciation_cost_center, depreciation_series = frappe.get_cached_value(
- "Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"]
- )
-
- depreciation_cost_center = asset.cost_center or depreciation_cost_center
-
- if not accounting_dimensions:
- accounting_dimensions = get_checks_for_pl_and_bs_accounts()
-
- depreciation_posting_error = None
-
- for d in asset_depr_schedule_doc.get("depreciation_schedule")[
- sch_start_idx or 0 : sch_end_idx or len(asset_depr_schedule_doc.get("depreciation_schedule"))
+ for d in depr_schedule_doc.get("depreciation_schedule")[
+ (sch_start_idx or 0) : (sch_end_idx or len(depr_schedule_doc.get("depreciation_schedule")))
]:
try:
_make_journal_entry_for_depreciation(
- asset_depr_schedule_doc,
+ depr_schedule_doc,
asset,
date,
d,
sch_start_idx,
sch_end_idx,
- depreciation_cost_center,
- depreciation_series,
+ depr_cost_center,
+ depr_series,
credit_account,
debit_account,
accounting_dimensions,
)
except Exception as e:
- depreciation_posting_error = e
+ depr_posting_error = e
+ asset.reload()
asset.set_status()
- if not depreciation_posting_error:
+ if not depr_posting_error:
asset.db_set("depr_entry_posting_status", "Successful")
- return asset_depr_schedule_doc
+ depr_schedule_doc.reload()
+ return depr_schedule_doc
- raise depreciation_posting_error
+ raise depr_posting_error
def _make_journal_entry_for_depreciation(
- asset_depr_schedule_doc,
+ depr_schedule_doc,
asset,
date,
depr_schedule,
sch_start_idx,
sch_end_idx,
- depreciation_cost_center,
- depreciation_series,
+ depr_cost_center,
+ depr_series,
credit_account,
debit_account,
accounting_dimensions,
@@ -272,19 +229,40 @@ def _make_journal_entry_for_depreciation(
return
je = frappe.new_doc("Journal Entry")
+ setup_journal_entry_metadata(je, depr_schedule_doc, depr_series, depr_schedule, asset)
+
+ credit_entry, debit_entry = get_credit_and_debit_entry(
+ credit_account, depr_schedule, asset, depr_cost_center, debit_account, accounting_dimensions
+ )
+
+ je.append("accounts", credit_entry)
+ je.append("accounts", debit_entry)
+
+ je.flags.ignore_permissions = True
+ je.save()
+
+ if not je.meta.get_workflow():
+ je.submit()
+
+
+def setup_journal_entry_metadata(je, depr_schedule_doc, depr_series, depr_schedule, asset):
je.voucher_type = "Depreciation Entry"
- je.naming_series = depreciation_series
+ je.naming_series = depr_series
je.posting_date = depr_schedule.schedule_date
je.company = asset.company
- je.finance_book = asset_depr_schedule_doc.finance_book
+ je.finance_book = depr_schedule_doc.finance_book
je.remark = f"Depreciation Entry against {asset.name} worth {depr_schedule.depreciation_amount}"
+
+def get_credit_and_debit_entry(
+ credit_account, depr_schedule, asset, depr_cost_center, debit_account, dimensions
+):
credit_entry = {
"account": credit_account,
"credit_in_account_currency": depr_schedule.depreciation_amount,
"reference_type": "Asset",
"reference_name": asset.name,
- "cost_center": depreciation_cost_center,
+ "cost_center": depr_cost_center,
}
debit_entry = {
@@ -292,42 +270,20 @@ def _make_journal_entry_for_depreciation(
"debit_in_account_currency": depr_schedule.depreciation_amount,
"reference_type": "Asset",
"reference_name": asset.name,
- "cost_center": depreciation_cost_center,
+ "cost_center": depr_cost_center,
}
- for dimension in accounting_dimensions:
+ for dimension in dimensions:
if asset.get(dimension["fieldname"]) or dimension.get("mandatory_for_bs"):
- credit_entry.update(
- {
- dimension["fieldname"]: asset.get(dimension["fieldname"])
- or dimension.get("default_dimension")
- }
+ credit_entry[dimension["fieldname"]] = asset.get(dimension["fieldname"]) or dimension.get(
+ "default_dimension"
)
if asset.get(dimension["fieldname"]) or dimension.get("mandatory_for_pl"):
- debit_entry.update(
- {
- dimension["fieldname"]: asset.get(dimension["fieldname"])
- or dimension.get("default_dimension")
- }
+ debit_entry[dimension["fieldname"]] = asset.get(dimension["fieldname"]) or dimension.get(
+ "default_dimension"
)
-
- je.append("accounts", credit_entry)
- je.append("accounts", debit_entry)
-
- je.flags.ignore_permissions = True
- je.flags.planned_depr_entry = True
- je.save()
-
- depr_schedule.db_set("journal_entry", je.name)
-
- if not je.meta.get_workflow():
- je.submit()
- asset.reload()
- idx = cint(asset_depr_schedule_doc.finance_book_id)
- row = asset.get("finance_books")[idx - 1]
- row.value_after_depreciation -= depr_schedule.depreciation_amount
- row.db_update()
+ return credit_entry, debit_entry
def get_depreciation_accounts(asset_category, company):
@@ -400,21 +356,7 @@ def notify_depr_entry_posting_error(failed_asset_names, error_log_names):
asset_links = get_comma_separated_links(failed_asset_names, "Asset")
error_log_links = get_comma_separated_links(error_log_names, "Error Log")
- message = (
- _("Hello,")
- + "
"
- + _("The following assets have failed to automatically post depreciation entries: {0}").format(
- asset_links
- )
- + "."
- + "
"
- + _("Here are the error logs for the aforementioned failed depreciation entries: {0}").format(
- error_log_links
- )
- + "."
- + "
"
- + _("Please share this email with your support team so that they can find and fix the issue.")
- )
+ message = get_message_for_depr_entry_posting_error(asset_links, error_log_links)
frappe.sendmail(recipients=recipients, subject=subject, message=message)
@@ -430,197 +372,184 @@ def get_comma_separated_links(names, doctype):
return links
+def get_message_for_depr_entry_posting_error(asset_links, error_log_links):
+ return (
+ _("Hello,")
+ + "
"
+ + _("The following assets have failed to automatically post depreciation entries: {0}").format(
+ asset_links
+ )
+ + "."
+ + "
"
+ + _("Here are the error logs for the aforementioned failed depreciation entries: {0}").format(
+ error_log_links
+ )
+ + "."
+ + "
"
+ + _("Please share this email with your support team so that they can find and fix the issue.")
+ )
+
+
@frappe.whitelist()
def scrap_asset(asset_name, scrap_date=None):
asset = frappe.get_doc("Asset", asset_name)
+ scrap_date = getdate(scrap_date) or getdate(today())
+ asset.db_set("disposal_date", scrap_date)
+ validate_asset_for_scrap(asset, scrap_date)
+ depreciate_asset(asset, scrap_date, get_note_for_scrap(asset))
+ asset.reload()
+
+ create_journal_entry_for_scrap(asset, scrap_date)
+
+
+def validate_asset_for_scrap(asset, scrap_date):
if asset.docstatus != 1:
frappe.throw(_("Asset {0} must be submitted").format(asset.name))
elif asset.status in ("Cancelled", "Sold", "Scrapped", "Capitalized"):
frappe.throw(_("Asset {0} cannot be scrapped, as it is already {1}").format(asset.name, asset.status))
- today_date = getdate(today())
- date = getdate(scrap_date) or today_date
- purchase_date = getdate(asset.purchase_date)
+ validate_scrap_date(asset, scrap_date)
- validate_scrap_date(date, today_date, purchase_date, asset.calculate_depreciation, asset_name)
- notes = _("This schedule was created when Asset {0} was scrapped.").format(
+def validate_scrap_date(asset, scrap_date):
+ if scrap_date > getdate():
+ frappe.throw(_("Future date is not allowed"))
+ elif scrap_date < getdate(asset.purchase_date):
+ frappe.throw(_("Scrap date cannot be before purchase date"))
+
+ if asset.calculate_depreciation:
+ last_booked_depreciation_date = get_last_depreciation_date(asset.name)
+ if (
+ last_booked_depreciation_date
+ and scrap_date < last_booked_depreciation_date
+ and scrap_date > getdate(asset.purchase_date)
+ ):
+ frappe.throw(_("Asset cannot be scrapped before the last depreciation entry."))
+
+
+def get_last_depreciation_date(asset_name):
+ depreciation = frappe.qb.DocType("Asset Depreciation Schedule")
+ depreciation_schedule = frappe.qb.DocType("Depreciation Schedule")
+
+ last_depreciation_date = (
+ frappe.qb.from_(depreciation)
+ .join(depreciation_schedule)
+ .on(depreciation.name == depreciation_schedule.parent)
+ .select(depreciation_schedule.schedule_date)
+ .where(depreciation.asset == asset_name)
+ .where(depreciation.docstatus == 1)
+ .where(depreciation_schedule.journal_entry != "")
+ .orderby(depreciation_schedule.schedule_date, order=Order.desc)
+ .limit(1)
+ .run()
+ )
+
+ return last_depreciation_date[0][0] if last_depreciation_date else None
+
+
+def get_note_for_scrap(asset):
+ return _("This schedule was created when Asset {0} was scrapped.").format(
get_link_to_form(asset.doctype, asset.name)
)
- if asset.status != "Fully Depreciated":
- depreciate_asset(asset, date, notes)
- asset.reload()
+
+def create_journal_entry_for_scrap(asset, scrap_date):
depreciation_series = frappe.get_cached_value("Company", asset.company, "series_for_depreciation_entry")
je = frappe.new_doc("Journal Entry")
- je.voucher_type = "Journal Entry"
+ je.voucher_type = "Asset Disposal"
je.naming_series = depreciation_series
- je.posting_date = date
+ je.posting_date = scrap_date
je.company = asset.company
- je.remark = f"Scrap Entry for asset {asset_name}"
+ je.remark = f"Scrap Entry for asset {asset.name}"
- for entry in get_gl_entries_on_asset_disposal(asset, date):
- entry.update({"reference_type": "Asset", "reference_name": asset_name})
+ for entry in get_gl_entries_on_asset_disposal(asset, scrap_date):
+ entry.update({"reference_type": "Asset", "reference_name": asset.name})
je.append("accounts", entry)
je.flags.ignore_permissions = True
- je.submit()
+ je.save()
+ if not je.meta.get_workflow():
+ je.submit()
- frappe.db.set_value("Asset", asset_name, "disposal_date", date)
- frappe.db.set_value("Asset", asset_name, "journal_entry_for_scrap", je.name)
- asset.set_status("Scrapped")
-
- add_asset_activity(asset_name, _("Asset scrapped"))
-
- frappe.msgprint(_("Asset scrapped via Journal Entry {0}").format(je.name))
-
-
-def validate_scrap_date(scrap_date, today_date, purchase_date, calculate_depreciation, asset_name):
- if scrap_date > today_date:
- frappe.throw(_("Future date is not allowed"))
- elif scrap_date < purchase_date:
- frappe.throw(_("Scrap date cannot be before purchase date"))
-
- if calculate_depreciation:
- asset_depreciation_schedules = frappe.db.get_all(
- "Asset Depreciation Schedule", filters={"asset": asset_name, "docstatus": 1}, fields=["name"]
- )
-
- for depreciation_schedule in asset_depreciation_schedules:
- last_booked_depreciation_date = frappe.db.get_value(
- "Depreciation Schedule",
- {
- "parent": depreciation_schedule["name"],
- "docstatus": 1,
- "journal_entry": ["!=", ""],
- },
- "schedule_date",
- order_by="schedule_date desc",
- )
- if (
- last_booked_depreciation_date
- and scrap_date < last_booked_depreciation_date
- and scrap_date > purchase_date
- ):
- frappe.throw(_("Asset cannot be scrapped before the last depreciation entry."))
+ add_asset_activity(asset.name, _("Asset scrapped"))
+ frappe.msgprint(
+ _("Asset scrapped via Journal Entry {0}").format(get_link_to_form("Journal Entry", je.name))
+ )
@frappe.whitelist()
def restore_asset(asset_name):
asset = frappe.get_doc("Asset", asset_name)
+ reverse_depreciation_entry_made_on_disposal(asset)
+ reset_depreciation_schedule(asset, get_note_for_restore(asset))
+ cancel_journal_entry_for_scrap(asset)
+ asset.set_status()
+ add_asset_activity(asset_name, _("Asset restored"))
- reverse_depreciation_entry_made_after_disposal(asset, asset.disposal_date)
- je = asset.journal_entry_for_scrap
-
- notes = _("This schedule was created when Asset {0} was restored.").format(
+def get_note_for_restore(asset):
+ return _("This schedule was created when Asset {0} was restored.").format(
get_link_to_form(asset.doctype, asset.name)
)
- reset_depreciation_schedule(asset, asset.disposal_date, notes)
- asset.db_set("disposal_date", None)
- asset.db_set("journal_entry_for_scrap", None)
-
- frappe.get_doc("Journal Entry", je).cancel()
-
- asset.set_status()
-
- add_asset_activity(asset_name, _("Asset restored"))
+def cancel_journal_entry_for_scrap(asset):
+ if asset.journal_entry_for_scrap:
+ je = asset.journal_entry_for_scrap
+ asset.db_set("disposal_date", None)
+ asset.db_set("journal_entry_for_scrap", None)
+ frappe.get_doc("Journal Entry", je).cancel()
def depreciate_asset(asset_doc, date, notes):
if not asset_doc.calculate_depreciation:
return
- asset_doc.flags.ignore_validate_update_after_submit = True
-
- make_new_active_asset_depr_schedules_and_cancel_current_ones(asset_doc, notes, date_of_disposal=date)
-
- asset_doc.save()
-
- make_depreciation_entry_for_all_asset_depr_schedules(asset_doc, date)
+ reschedule_depreciation(asset_doc, notes, disposal_date=date)
+ make_depreciation_entry_on_disposal(asset_doc, date)
+ # As per Income Tax Act (India), the asset should not be depreciated
+ # in the financial year in which it is sold/scraped
asset_doc.reload()
cancel_depreciation_entries(asset_doc, date)
@erpnext.allow_regional
def cancel_depreciation_entries(asset_doc, date):
+ # Cancel all depreciation entries for the current financial year
+ # if the asset is sold/scraped in the current financial year
+ # Overwritten via India Compliance app
pass
-def reset_depreciation_schedule(asset_doc, date, notes):
- if not asset_doc.calculate_depreciation:
- return
-
- asset_doc.flags.ignore_validate_update_after_submit = True
-
- make_new_active_asset_depr_schedules_and_cancel_current_ones(asset_doc, notes, date_of_return=date)
-
- modify_depreciation_schedule_for_asset_repairs(asset_doc, notes)
-
- asset_doc.save()
+def reset_depreciation_schedule(asset_doc, notes):
+ if asset_doc.calculate_depreciation:
+ reschedule_depreciation(asset_doc, notes)
+ asset_doc.set_total_booked_depreciations()
-def modify_depreciation_schedule_for_asset_repairs(asset, notes):
- asset_repairs = frappe.get_all(
- "Asset Repair", filters={"asset": asset.name}, fields=["name", "increase_in_asset_life"]
- )
-
- for repair in asset_repairs:
- if repair.increase_in_asset_life:
- asset_repair = frappe.get_doc("Asset Repair", repair.name)
- asset_repair.modify_depreciation_schedule()
- make_new_active_asset_depr_schedules_and_cancel_current_ones(asset, notes)
-
-
-def reverse_depreciation_entry_made_after_disposal(asset, date):
+def reverse_depreciation_entry_made_on_disposal(asset):
for row in asset.get("finance_books"):
- asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active", row.finance_book)
- if not asset_depr_schedule_doc or not asset_depr_schedule_doc.get("depreciation_schedule"):
+ schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active", row.finance_book)
+ if not schedule_doc or not schedule_doc.get("depreciation_schedule"):
continue
- for schedule_idx, schedule in enumerate(asset_depr_schedule_doc.get("depreciation_schedule")):
- if schedule.schedule_date == date and schedule.journal_entry:
+ for schedule_idx, schedule in enumerate(schedule_doc.get("depreciation_schedule")):
+ if schedule.schedule_date == asset.disposal_date and schedule.journal_entry:
if not disposal_was_made_on_original_schedule_date(
- schedule_idx, row, date
- ) or disposal_happens_in_the_future(date):
- reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry)
- reverse_journal_entry.posting_date = nowdate()
-
- for account in reverse_journal_entry.accounts:
- account.update(
- {
- "reference_type": "Asset",
- "reference_name": asset.name,
- }
- )
-
- frappe.flags.is_reverse_depr_entry = True
- reverse_journal_entry.submit()
-
- frappe.flags.is_reverse_depr_entry = False
- asset_depr_schedule_doc.flags.ignore_validate_update_after_submit = True
- asset.flags.ignore_validate_update_after_submit = True
- schedule.journal_entry = None
- depreciation_amount = get_depreciation_amount_in_je(reverse_journal_entry)
- row.value_after_depreciation += depreciation_amount
- asset_depr_schedule_doc.save()
- asset.save()
+ schedule_idx, row, asset.disposal_date
+ ) or disposal_happens_in_the_future(asset.disposal_date):
+ je = create_reverse_depreciation_entry(asset.name, schedule.journal_entry)
+ update_value_after_depreciation_on_asset_restore(schedule, row, je)
-def get_depreciation_amount_in_je(journal_entry):
- if journal_entry.accounts[0].debit_in_account_currency:
- return journal_entry.accounts[0].debit_in_account_currency
- else:
- return journal_entry.accounts[0].credit_in_account_currency
-
-
-# if the invoice had been posted on the date the depreciation was initially supposed to happen, the depreciation shouldn't be undone
-def disposal_was_made_on_original_schedule_date(schedule_idx, row, posting_date_of_disposal):
+def disposal_was_made_on_original_schedule_date(schedule_idx, row, disposal_date):
+ """
+ If asset is scrapped or sold on original schedule date,
+ then the depreciation entry should not be reversed.
+ """
orginal_schedule_date = add_months(
row.depreciation_start_date, schedule_idx * cint(row.frequency_of_depreciation)
)
@@ -628,19 +557,57 @@ def disposal_was_made_on_original_schedule_date(schedule_idx, row, posting_date_
if is_last_day_of_the_month(row.depreciation_start_date):
orginal_schedule_date = get_last_day(orginal_schedule_date)
- if orginal_schedule_date == posting_date_of_disposal:
+ if orginal_schedule_date == disposal_date:
return True
return False
-def disposal_happens_in_the_future(posting_date_of_disposal):
- if posting_date_of_disposal > getdate():
+def disposal_happens_in_the_future(disposal_date):
+ if disposal_date > getdate():
return True
return False
+def create_reverse_depreciation_entry(asset_name, journal_entry):
+ reverse_journal_entry = make_reverse_journal_entry(journal_entry)
+ reverse_journal_entry.posting_date = nowdate()
+
+ for account in reverse_journal_entry.accounts:
+ account.update(
+ {
+ "reference_type": "Asset",
+ "reference_name": asset_name,
+ }
+ )
+
+ frappe.flags.is_reverse_depr_entry = True
+ if not reverse_journal_entry.meta.get_workflow():
+ reverse_journal_entry.submit()
+ return reverse_journal_entry
+ else:
+ frappe.throw(
+ _("Please disable workflow temporarily for Journal Entry {0}").format(reverse_journal_entry.name)
+ )
+
+
+def update_value_after_depreciation_on_asset_restore(schedule, row, journal_entry):
+ frappe.db.set_value("Depreciation Schedule", schedule.name, "journal_entry", None, update_modified=False)
+ depreciation_amount = get_depreciation_amount_in_je(journal_entry)
+ value_after_depreciation = flt(
+ row.value_after_depreciation + depreciation_amount, row.precision("value_after_depreciation")
+ )
+ row.db_set("value_after_depreciation", value_after_depreciation)
+
+
+def get_depreciation_amount_in_je(journal_entry):
+ if journal_entry.accounts[0].debit_in_account_currency:
+ return journal_entry.accounts[0].debit_in_account_currency
+ else:
+ return journal_entry.accounts[0].credit_in_account_currency
+
+
def get_gl_entries_on_asset_regain(
asset, selling_amount=0, finance_book=None, voucher_type=None, voucher_no=None, date=None
):
@@ -874,9 +841,7 @@ def get_value_after_depreciation_on_disposal_date(asset, disposal_date, finance_
row = asset_doc.finance_books[idx - 1]
- temp_asset_depreciation_schedule = get_temp_asset_depr_schedule_doc(
- asset_doc, row, getdate(disposal_date)
- )
+ temp_asset_depreciation_schedule = get_temp_depr_schedule_doc(asset_doc, row, getdate(disposal_date))
accumulated_depr_amount = temp_asset_depreciation_schedule.get("depreciation_schedule")[
-1
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index 3f984d9326e..9e2207367ba 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -30,11 +30,8 @@ from erpnext.assets.doctype.asset.depreciation import (
scrap_asset,
)
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
- _check_is_pro_rata,
- _get_pro_rata_amt,
get_asset_depr_schedule_doc,
get_depr_schedule,
- get_depreciation_amount,
)
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
make_purchase_invoice as make_invoice,
@@ -196,7 +193,7 @@ class TestAsset(AssetSetup):
self.assertEqual(doc.items[0].is_fixed_asset, 1)
def test_scrap_asset(self):
- date = nowdate()
+ date = "2025-05-05"
purchase_date = add_months(get_first_day(date), -2)
asset = create_asset(
@@ -246,7 +243,7 @@ class TestAsset(AssetSetup):
frappe.ValidationError, scrap_asset, asset.name, scrap_date=before_last_booked_depreciation_date
)
- scrap_asset(asset.name)
+ scrap_asset(asset.name, date)
asset.load_from_db()
first_asset_depr_schedule.load_from_db()
@@ -258,12 +255,16 @@ class TestAsset(AssetSetup):
asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation,
asset.precision("gross_purchase_amount"),
)
- pro_rata_amount, _, _ = _get_pro_rata_amt(
- asset.finance_books[0],
- 9000,
+
+ second_asset_depr_schedule.depreciation_amount = 9006.17
+ second_asset_depr_schedule.asset_doc = asset
+ second_asset_depr_schedule.get_finance_book_row()
+ second_asset_depr_schedule.fetch_asset_details()
+
+ pro_rata_amount, _, _ = second_asset_depr_schedule._get_pro_rata_amt(
add_days(get_last_day(add_months(purchase_date, 1)), 1),
date,
- original_schedule_date=get_last_day(nowdate()),
+ original_schedule_date=get_last_day(date),
)
pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount"))
self.assertEqual(
@@ -331,7 +332,7 @@ class TestAsset(AssetSetup):
si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company")
si.customer = "_Test Customer"
- si.due_date = nowdate()
+ si.due_date = date
si.get("items")[0].rate = 25000
si.insert()
si.submit()
@@ -344,19 +345,17 @@ class TestAsset(AssetSetup):
self.assertEqual(second_asset_depr_schedule.status, "Active")
self.assertEqual(first_asset_depr_schedule.status, "Cancelled")
- pro_rata_amount, _, _ = _get_pro_rata_amt(
- asset.finance_books[0],
- 9000,
- add_days(get_last_day(add_months(purchase_date, 1)), 1),
- date,
- original_schedule_date=get_last_day(nowdate()),
+ asset.load_from_db()
+ accumulated_depr_amount = flt(
+ asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation,
+ asset.precision("gross_purchase_amount"),
)
- pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount"))
+ pro_rata_amount = flt(accumulated_depr_amount - 18000)
expected_gle = (
(
"_Test Accumulated Depreciations - _TC",
- flt(18000.0 + pro_rata_amount, asset.precision("gross_purchase_amount")),
+ flt(accumulated_depr_amount, asset.precision("gross_purchase_amount")),
0.0,
),
("_Test Fixed Asset - _TC", 0.0, 100000.0),
@@ -476,26 +475,26 @@ class TestAsset(AssetSetup):
asset = create_asset(
calculate_depreciation=1,
asset_quantity=10,
- available_for_use_date="2020-01-01",
- purchase_date="2020-01-01",
+ available_for_use_date="2023-01-01",
+ purchase_date="2023-01-01",
expected_value_after_useful_life=0,
total_number_of_depreciations=6,
opening_number_of_booked_depreciations=1,
- frequency_of_depreciation=10,
- depreciation_start_date="2021-01-01",
- opening_accumulated_depreciation=20000,
- gross_purchase_amount=120000,
+ frequency_of_depreciation=12,
+ depreciation_start_date="2024-03-31",
+ opening_accumulated_depreciation=493.15,
+ gross_purchase_amount=12000,
submit=1,
)
first_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active")
self.assertEqual(first_asset_depr_schedule.status, "Active")
- post_depreciation_entries(date="2021-01-01")
+ post_depreciation_entries(date="2024-03-31")
self.assertEqual(asset.asset_quantity, 10)
- self.assertEqual(asset.gross_purchase_amount, 120000)
- self.assertEqual(asset.opening_accumulated_depreciation, 20000)
+ self.assertEqual(asset.gross_purchase_amount, 12000)
+ self.assertEqual(asset.opening_accumulated_depreciation, 493.15)
new_asset = split_asset(asset.name, 2)
asset.load_from_db()
@@ -511,25 +510,25 @@ class TestAsset(AssetSetup):
depr_schedule_of_new_asset = first_asset_depr_schedule_of_new_asset.get("depreciation_schedule")
self.assertEqual(new_asset.asset_quantity, 2)
- self.assertEqual(new_asset.gross_purchase_amount, 24000)
- self.assertEqual(new_asset.opening_accumulated_depreciation, 4000)
+ self.assertEqual(new_asset.gross_purchase_amount, 2400)
+ self.assertEqual(new_asset.opening_accumulated_depreciation, 98.63)
self.assertEqual(new_asset.split_from, asset.name)
- self.assertEqual(depr_schedule_of_new_asset[0].depreciation_amount, 4000)
- self.assertEqual(depr_schedule_of_new_asset[1].depreciation_amount, 4000)
+ self.assertEqual(depr_schedule_of_new_asset[0].depreciation_amount, 400)
+ self.assertEqual(depr_schedule_of_new_asset[1].depreciation_amount, 400)
self.assertEqual(asset.asset_quantity, 8)
- self.assertEqual(asset.gross_purchase_amount, 96000)
- self.assertEqual(asset.opening_accumulated_depreciation, 16000)
- self.assertEqual(depr_schedule_of_asset[0].depreciation_amount, 16000)
- self.assertEqual(depr_schedule_of_asset[1].depreciation_amount, 16000)
+ self.assertEqual(asset.gross_purchase_amount, 9600)
+ self.assertEqual(asset.opening_accumulated_depreciation, 394.52)
+ self.assertEqual(depr_schedule_of_asset[0].depreciation_amount, 1600)
+ self.assertEqual(depr_schedule_of_asset[1].depreciation_amount, 1600)
journal_entry = depr_schedule_of_asset[0].journal_entry
jv = frappe.get_doc("Journal Entry", journal_entry)
- self.assertEqual(jv.accounts[0].credit_in_account_currency, 16000)
- 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].credit_in_account_currency, 1600)
+ self.assertEqual(jv.accounts[1].debit_in_account_currency, 1600)
+ self.assertEqual(jv.accounts[2].credit_in_account_currency, 400)
+ self.assertEqual(jv.accounts[3].debit_in_account_currency, 400)
self.assertEqual(jv.accounts[0].reference_name, asset.name)
self.assertEqual(jv.accounts[1].reference_name, asset.name)
@@ -944,12 +943,12 @@ class TestDepreciationMethods(AssetSetup):
)
expected_schedules = [
- ["2022-02-28", 310.89, 310.89],
- ["2022-03-31", 654.45, 965.34],
- ["2022-04-30", 654.45, 1619.79],
- ["2022-05-31", 654.45, 2274.24],
- ["2022-06-30", 654.45, 2928.69],
- ["2022-07-15", 2071.31, 5000.0],
+ ["2022-02-28", 337.72, 337.72],
+ ["2022-03-31", 675.45, 1013.17],
+ ["2022-04-30", 675.45, 1688.62],
+ ["2022-05-31", 675.45, 2364.07],
+ ["2022-06-30", 675.45, 3039.52],
+ ["2022-07-15", 1960.48, 5000.0],
]
schedules = [
@@ -1024,12 +1023,17 @@ class TestDepreciationBasics(AssetSetup):
"depreciation_start_date": "2020-12-31",
},
)
+ asset.submit()
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active")
+ asset_depr_schedule_doc.asset_doc = asset
+ asset_depr_schedule_doc.get_finance_book_row()
+ asset_depr_schedule_doc.fetch_asset_details()
+ asset_depr_schedule_doc.clear()
+ asset_depr_schedule_doc._check_is_pro_rata()
+ asset_depr_schedule_doc.initialize_variables()
- depreciation_amount, prev_per_day_depr = get_depreciation_amount(
- asset_depr_schedule_doc, asset, 100000, 100000, asset.finance_books[0]
- )
+ depreciation_amount = asset_depr_schedule_doc.get_depreciation_amount(0)
self.assertEqual(depreciation_amount, 30000)
def test_make_depr_schedule(self):
@@ -1074,7 +1078,7 @@ class TestDepreciationBasics(AssetSetup):
def test_check_is_pro_rata(self):
"""Tests if check_is_pro_rata() returns the right value(i.e. checks if has_pro_rata is accurate)."""
- asset = create_asset(item_code="Macbook Pro", available_for_use_date="2019-12-31", do_not_save=1)
+ asset = create_asset(item_code="Macbook Pro", available_for_use_date="2019-12-31")
asset.calculate_depreciation = 1
asset.append(
@@ -1087,9 +1091,15 @@ class TestDepreciationBasics(AssetSetup):
"depreciation_start_date": "2020-12-31",
},
)
+ asset.save()
- has_pro_rata = _check_is_pro_rata(asset, asset.finance_books[0])
- self.assertFalse(has_pro_rata)
+ depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Draft")
+ depr_schedule_doc.asset_doc = asset
+ depr_schedule_doc.get_finance_book_row()
+ depr_schedule_doc.fetch_asset_details()
+ depr_schedule_doc._check_is_pro_rata()
+
+ self.assertFalse(depr_schedule_doc.has_pro_rata)
asset.finance_books = []
asset.append(
@@ -1102,9 +1112,15 @@ class TestDepreciationBasics(AssetSetup):
"depreciation_start_date": "2020-07-01",
},
)
+ asset.save()
- has_pro_rata = _check_is_pro_rata(asset, asset.finance_books[0])
- self.assertTrue(has_pro_rata)
+ depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Draft")
+ depr_schedule_doc.asset_doc = asset
+ depr_schedule_doc.get_finance_book_row()
+ depr_schedule_doc.fetch_asset_details()
+ depr_schedule_doc._check_is_pro_rata()
+
+ self.assertTrue(depr_schedule_doc.has_pro_rata)
def test_expected_value_after_useful_life_greater_than_purchase_amount(self):
"""Tests if an error is raised when expected_value_after_useful_life(110,000) > gross_purchase_amount(100,000)."""
@@ -1285,8 +1301,6 @@ class TestDepreciationBasics(AssetSetup):
self.assertFalse(entry["debit"])
def test_depr_entry_posting_when_depr_expense_account_is_an_income_account(self):
- """Tests if the Depreciation Expense Account gets credited and the Accumulated Depreciation Account gets debited when the former's an Income Account."""
-
depr_expense_account = frappe.get_doc("Account", "_Test Depreciations - _TC")
depr_expense_account.root_type = "Income"
depr_expense_account.parent_account = "Income - _TC"
@@ -1303,26 +1317,20 @@ class TestDepreciationBasics(AssetSetup):
submit=1,
)
- post_depreciation_entries(date="2021-06-01")
- asset.load_from_db()
+ jv = make_journal_entry(
+ "_Test Depreciations - _TC",
+ "_Test Accumulated Depreciations - _TC",
+ 100,
+ posting_date="2020-01-15",
+ save=False,
+ )
+ for d in jv.accounts:
+ d.reference_type = "Asset"
+ d.reference_name = asset.name
+ jv.voucher_type = "Depreciation Entry"
- je = frappe.get_doc("Journal Entry", get_depr_schedule(asset.name, "Active")[0].journal_entry)
- accounting_entries = [
- {"account": entry.account, "debit": entry.debit, "credit": entry.credit} for entry in je.accounts
- ]
-
- for entry in accounting_entries:
- if entry["account"] == "_Test Depreciations - _TC":
- self.assertTrue(entry["credit"])
- self.assertFalse(entry["debit"])
- else:
- self.assertTrue(entry["debit"])
- self.assertFalse(entry["credit"])
-
- # resetting
- depr_expense_account.root_type = "Expense"
- depr_expense_account.parent_account = "Expenses - _TC"
- depr_expense_account.save()
+ with self.assertRaises(frappe.ValidationError):
+ jv.insert()
def test_clear_depr_schedule(self):
"""Tests if clear_depr_schedule() works as expected."""
@@ -1343,7 +1351,7 @@ class TestDepreciationBasics(AssetSetup):
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active")
- asset_depr_schedule_doc.clear_depr_schedule()
+ asset_depr_schedule_doc.clear()
self.assertEqual(len(asset_depr_schedule_doc.get("depreciation_schedule")), 1)
@@ -1390,15 +1398,15 @@ class TestDepreciationBasics(AssetSetup):
asset.load_from_db()
asset_depr_schedule_doc_1 = get_asset_depr_schedule_doc(asset.name, "Active", "Test Finance Book 1")
- asset_depr_schedule_doc_1.clear_depr_schedule()
+ asset_depr_schedule_doc_1.clear()
self.assertEqual(len(asset_depr_schedule_doc_1.get("depreciation_schedule")), 3)
asset_depr_schedule_doc_2 = get_asset_depr_schedule_doc(asset.name, "Active", "Test Finance Book 2")
- asset_depr_schedule_doc_2.clear_depr_schedule()
+ asset_depr_schedule_doc_2.clear()
self.assertEqual(len(asset_depr_schedule_doc_2.get("depreciation_schedule")), 3)
asset_depr_schedule_doc_3 = get_asset_depr_schedule_doc(asset.name, "Active", "Test Finance Book 3")
- asset_depr_schedule_doc_3.clear_depr_schedule()
+ asset_depr_schedule_doc_3.clear()
self.assertEqual(len(asset_depr_schedule_doc_3.get("depreciation_schedule")), 0)
def test_depreciation_schedules_are_set_up_for_multiple_finance_books(self):
@@ -1448,6 +1456,11 @@ class TestDepreciationBasics(AssetSetup):
submit=1,
)
+ depr_expense_account = frappe.get_doc("Account", "_Test Depreciations - _TC")
+ depr_expense_account.root_type = "Expense"
+ depr_expense_account.parent_account = "Expenses - _TC"
+ depr_expense_account.save()
+
post_depreciation_entries(date="2021-01-01")
asset.load_from_db()
@@ -1519,7 +1532,7 @@ class TestDepreciationBasics(AssetSetup):
)
self.assertSequenceEqual(gle, expected_gle)
- self.assertEqual(asset.get("value_after_depreciation"), 0)
+ self.assertEqual(asset.get("value_after_depreciation"), 70000)
def test_expected_value_change(self):
"""
@@ -1616,6 +1629,10 @@ class TestDepreciationBasics(AssetSetup):
frequency_of_depreciation=1,
submit=1,
)
+ depr_expense_account = frappe.get_doc("Account", "_Test Depreciations - _TC")
+ depr_expense_account.root_type = "Expense"
+ depr_expense_account.parent_account = "Expenses - _TC"
+ depr_expense_account.save()
self.assertEqual(asset.status, "Submitted")
self.assertEqual(asset.get_value_after_depreciation(), 100000)
diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
index e01c722b0d4..b8b285281e0 100644
--- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
+++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
@@ -16,7 +16,7 @@ from erpnext.assets.doctype.asset.depreciation import (
get_gl_entries_on_asset_disposal,
get_value_after_depreciation_on_disposal_date,
reset_depreciation_schedule,
- reverse_depreciation_entry_made_after_disposal,
+ reverse_depreciation_entry_made_on_disposal,
)
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
@@ -140,6 +140,7 @@ class AssetCapitalization(StockController):
self.make_gl_entries()
self.repost_future_sle_and_gle()
self.restore_consumed_asset_items()
+ self.update_target_asset()
def set_title(self):
self.title = self.target_asset_name or self.target_item_name or self.target_item_code
@@ -602,13 +603,18 @@ class AssetCapitalization(StockController):
return
total_target_asset_value = flt(self.total_value, self.precision("total_value"))
-
asset_doc = frappe.get_doc("Asset", self.target_asset)
- asset_doc.gross_purchase_amount += total_target_asset_value
- asset_doc.purchase_amount += total_target_asset_value
- asset_doc.set_status("Work In Progress")
- asset_doc.flags.ignore_validate = True
- asset_doc.save()
+
+ if self.docstatus == 2:
+ gross_purchase_amount = asset_doc.gross_purchase_amount - total_target_asset_value
+ purchase_amount = asset_doc.purchase_amount - total_target_asset_value
+ asset_doc.db_set("total_asset_cost", asset_doc.total_asset_cost - total_target_asset_value)
+ else:
+ gross_purchase_amount = asset_doc.gross_purchase_amount + total_target_asset_value
+ purchase_amount = asset_doc.purchase_amount + total_target_asset_value
+
+ asset_doc.db_set("gross_purchase_amount", gross_purchase_amount)
+ asset_doc.db_set("purchase_amount", purchase_amount)
frappe.msgprint(
_("Asset {0} has been updated. Please set the depreciation details if any and submit it.").format(
@@ -619,17 +625,17 @@ class AssetCapitalization(StockController):
def restore_consumed_asset_items(self):
for item in self.asset_items:
asset = frappe.get_doc("Asset", item.asset)
- asset.db_set("disposal_date", None)
self.set_consumed_asset_status(asset)
if asset.calculate_depreciation:
- reverse_depreciation_entry_made_after_disposal(asset, self.posting_date)
+ reverse_depreciation_entry_made_on_disposal(asset)
notes = _(
"This schedule was created when Asset {0} was restored on Asset Capitalization {1}'s cancellation."
).format(
get_link_to_form(asset.doctype, asset.name), get_link_to_form(self.doctype, self.name)
)
- reset_depreciation_schedule(asset, self.posting_date, notes)
+ reset_depreciation_schedule(asset, notes)
+ asset.db_set("disposal_date", None)
def set_consumed_asset_status(self, asset):
if self.docstatus == 1:
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 3f7a2e7c7d8..f76f1d11720 100644
--- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.js
+++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.js
@@ -31,7 +31,7 @@ frappe.ui.form.on("Depreciation Schedule", {
frappe.call({
method: "erpnext.assets.doctype.asset.depreciation.make_depreciation_entry",
args: {
- asset_depr_schedule_name: frm.doc.name,
+ depr_schedule_name: frm.doc.name,
date: row.schedule_date,
},
debounce: 1000,
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 76565cb4e38..fb075df4acc 100644
--- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json
+++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json
@@ -25,6 +25,7 @@
"column_break_8",
"frequency_of_depreciation",
"expected_value_after_useful_life",
+ "value_after_depreciation",
"depreciation_schedule_section",
"depreciation_schedule",
"details_section",
@@ -38,6 +39,7 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Asset",
+ "link_filters": "[[\"Asset\",\"docstatus\",\"<\",\"2\"],[\"Asset\",\"company\",\"=\",\"eval:doc.company\"]]",
"options": "Asset",
"reqd": 1
},
@@ -202,12 +204,18 @@
"label": "Company",
"options": "Company",
"read_only": 1
+ },
+ {
+ "fieldname": "value_after_depreciation",
+ "fieldtype": "Currency",
+ "label": "Value After Depreciation",
+ "read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2024-03-27 13:06:34.135004",
+ "modified": "2024-12-02 17:54:20.635668",
"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 5bddea56183..1750a2a782f 100644
--- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
+++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
@@ -3,27 +3,20 @@
import frappe
from frappe import _
-from frappe.model.document import Document
from frappe.utils import (
- add_days,
- add_months,
- add_years,
- cint,
- date_diff,
flt,
get_first_day,
- get_last_day,
get_link_to_form,
getdate,
- is_last_day_of_the_month,
- month_diff,
)
import erpnext
-from erpnext.accounts.utils import get_fiscal_year
+from erpnext.assets.doctype.asset_depreciation_schedule.deppreciation_schedule_controller import (
+ DepreciationScheduleController,
+)
-class AssetDepreciationSchedule(Document):
+class AssetDepreciationSchedule(DepreciationScheduleController):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
@@ -32,9 +25,7 @@ class AssetDepreciationSchedule(Document):
if TYPE_CHECKING:
from frappe.types import DF
- from erpnext.assets.doctype.depreciation_schedule.depreciation_schedule import (
- DepreciationSchedule,
- )
+ from erpnext.assets.doctype.depreciation_schedule.depreciation_schedule import DepreciationSchedule
amended_from: DF.Link | None
asset: DF.Link
@@ -51,23 +42,20 @@ class AssetDepreciationSchedule(Document):
gross_purchase_amount: DF.Currency
naming_series: DF.Literal["ACC-ADS-.YYYY.-"]
notes: DF.SmallText | None
- opening_number_of_booked_depreciations: DF.Int
opening_accumulated_depreciation: DF.Currency
+ opening_number_of_booked_depreciations: DF.Int
rate_of_depreciation: DF.Percent
shift_based: DF.Check
status: DF.Literal["Draft", "Active", "Cancelled"]
total_number_of_depreciations: DF.Int
+ value_after_depreciation: DF.Currency
# end: auto-generated types
- def before_save(self):
- if not self.finance_book_id:
- self.prepare_draft_asset_depr_schedule_data_from_asset_name_and_fb_name(
- self.asset, self.finance_book
- )
- self.update_shift_depr_schedule()
-
def validate(self):
self.validate_another_asset_depr_schedule_does_not_exist()
+ if not self.finance_book_id:
+ self.create_depreciation_schedule()
+ self.update_shift_depr_schedule()
def validate_another_asset_depr_schedule_does_not_exist(self):
finance_book_filter = ["finance_book", "is", "not set"]
@@ -100,7 +88,8 @@ class AssetDepreciationSchedule(Document):
def on_submit(self):
self.db_set("status", "Active")
- def before_cancel(self):
+ def on_cancel(self):
+ self.db_set("status", "Cancelled")
if not self.flags.should_not_cancel_depreciation_entries:
self.cancel_depreciation_entries()
@@ -109,923 +98,56 @@ class AssetDepreciationSchedule(Document):
if d.journal_entry:
frappe.get_doc("Journal Entry", d.journal_entry).cancel()
- def on_cancel(self):
- self.db_set("status", "Cancelled")
-
def update_shift_depr_schedule(self):
- if not self.shift_based or self.docstatus != 0:
+ if not self.shift_based or self.docstatus != 0 or self.get("__islocal"):
+ return
+ self.create_depreciation_schedule()
+
+ def get_finance_book_row(self, fb_row=None):
+ if fb_row:
+ self.fb_row = fb_row
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)
-
finance_book_filter = ["finance_book", "is", "not set"]
- if fb_name:
- finance_book_filter = ["finance_book", "=", fb_name]
+ if self.finance_book:
+ finance_book_filter = ["finance_book", "=", self.finance_book]
asset_finance_book_name = frappe.db.get_value(
doctype="Asset Finance Book",
- filters=[["parent", "=", asset_name], finance_book_filter],
+ filters=[["parent", "=", self.asset], finance_book_filter],
)
- asset_finance_book_doc = frappe.get_doc("Asset Finance Book", asset_finance_book_name)
+ self.fb_row = frappe.get_doc("Asset Finance Book", asset_finance_book_name)
- self.prepare_draft_asset_depr_schedule_data(asset_doc, asset_finance_book_doc)
-
- def prepare_draft_asset_depr_schedule_data(
- self,
- asset_doc,
- row,
- date_of_disposal=None,
- date_of_return=None,
- update_asset_finance_book_row=True,
- ):
- have_asset_details_been_modified = self.have_asset_details_been_modified(asset_doc)
- not_manual_depr_or_have_manual_depr_details_been_modified = (
- self.not_manual_depr_or_have_manual_depr_details_been_modified(row)
+ def fetch_asset_details(self):
+ self.asset = self.asset_doc.name
+ self.finance_book = self.fb_row.get("finance_book")
+ self.finance_book_id = self.fb_row.idx
+ self.opening_accumulated_depreciation = self.asset_doc.opening_accumulated_depreciation or 0
+ self.opening_number_of_booked_depreciations = (
+ self.asset_doc.opening_number_of_booked_depreciations or 0
)
-
- self.set_draft_asset_depr_schedule_details(asset_doc, row)
-
- if self.should_prepare_depreciation_schedule(
- have_asset_details_been_modified, not_manual_depr_or_have_manual_depr_details_been_modified
- ):
- self.make_depr_schedule(asset_doc, row, date_of_disposal, update_asset_finance_book_row)
- self.set_accumulated_depreciation(asset_doc, row, date_of_disposal, date_of_return)
-
- def have_asset_details_been_modified(self, asset_doc):
- return (
- asset_doc.gross_purchase_amount != self.gross_purchase_amount
- or asset_doc.opening_accumulated_depreciation != self.opening_accumulated_depreciation
- or asset_doc.opening_number_of_booked_depreciations != self.opening_number_of_booked_depreciations
- )
-
- def not_manual_depr_or_have_manual_depr_details_been_modified(self, row):
- return (
- self.depreciation_method != "Manual"
- or row.total_number_of_depreciations != self.total_number_of_depreciations
- or row.frequency_of_depreciation != self.frequency_of_depreciation
- or getdate(row.depreciation_start_date) != self.get("depreciation_schedule")[0].schedule_date
- or row.expected_value_after_useful_life != self.expected_value_after_useful_life
- )
-
- def should_prepare_depreciation_schedule(
- self, have_asset_details_been_modified, not_manual_depr_or_have_manual_depr_details_been_modified
- ):
- if not self.get("depreciation_schedule"):
- return True
-
- old_asset_depr_schedule_doc = self.get_doc_before_save()
-
- if self.docstatus != 0 and not old_asset_depr_schedule_doc:
- return True
-
- if have_asset_details_been_modified or not_manual_depr_or_have_manual_depr_details_been_modified:
- return True
-
- return False
-
- def set_draft_asset_depr_schedule_details(self, asset_doc, row):
- self.asset = asset_doc.name
- self.finance_book = row.finance_book
- self.finance_book_id = row.idx
- self.opening_accumulated_depreciation = asset_doc.opening_accumulated_depreciation or 0
- self.opening_number_of_booked_depreciations = asset_doc.opening_number_of_booked_depreciations or 0
- self.gross_purchase_amount = asset_doc.gross_purchase_amount
- self.depreciation_method = row.depreciation_method
- self.total_number_of_depreciations = row.total_number_of_depreciations
- self.frequency_of_depreciation = row.frequency_of_depreciation
- self.rate_of_depreciation = row.rate_of_depreciation
- self.expected_value_after_useful_life = row.expected_value_after_useful_life
- self.daily_prorata_based = row.daily_prorata_based
- self.shift_based = row.shift_based
+ self.gross_purchase_amount = self.asset_doc.gross_purchase_amount
+ self.depreciation_method = self.fb_row.depreciation_method
+ self.total_number_of_depreciations = self.fb_row.total_number_of_depreciations
+ self.frequency_of_depreciation = self.fb_row.frequency_of_depreciation
+ self.rate_of_depreciation = self.fb_row.get("rate_of_depreciation")
+ self.value_after_depreciation = self.fb_row.value_after_depreciation
+ self.expected_value_after_useful_life = self.fb_row.get("expected_value_after_useful_life")
+ self.daily_prorata_based = self.fb_row.get("daily_prorata_based")
+ self.shift_based = self.fb_row.get("shift_based")
self.status = "Draft"
- def make_depr_schedule(
- self,
- asset_doc,
- row,
- date_of_disposal=None,
- update_asset_finance_book_row=True,
- value_after_depreciation=None,
- ):
- if not self.get("depreciation_schedule"):
- self.depreciation_schedule = []
-
- if not asset_doc.available_for_use_date:
- return
-
- start = self.clear_depr_schedule()
-
- self._make_depr_schedule(
- asset_doc, row, start, date_of_disposal, update_asset_finance_book_row, value_after_depreciation
- )
-
- def clear_depr_schedule(self):
- start = 0
- num_of_depreciations_completed = 0
- depr_schedule = []
-
- self.schedules_before_clearing = self.get("depreciation_schedule")
-
- for schedule in self.get("depreciation_schedule"):
- if schedule.journal_entry:
- num_of_depreciations_completed += 1
- depr_schedule.append(schedule)
- else:
- start = num_of_depreciations_completed
- break
-
- self.depreciation_schedule = depr_schedule
-
- return start
-
- def _make_depr_schedule(
- self,
- asset_doc,
- row,
- start,
- date_of_disposal,
- update_asset_finance_book_row,
- value_after_depreciation,
- ):
- asset_doc.validate_asset_finance_books(row)
-
- if not value_after_depreciation:
- value_after_depreciation = _get_value_after_depreciation_for_making_schedule(asset_doc, row)
- row.value_after_depreciation = value_after_depreciation
-
- if update_asset_finance_book_row:
- row.db_update()
-
- final_number_of_depreciations = cint(row.total_number_of_depreciations) - cint(
- self.opening_number_of_booked_depreciations
- )
-
- has_pro_rata = _check_is_pro_rata(asset_doc, row)
- if has_pro_rata:
- final_number_of_depreciations += 1
-
- has_wdv_or_dd_non_yearly_pro_rata = False
- if (
- row.depreciation_method in ("Written Down Value", "Double Declining Balance")
- and cint(row.frequency_of_depreciation) != 12
- ):
- has_wdv_or_dd_non_yearly_pro_rata = _check_is_pro_rata(asset_doc, row, wdv_or_dd_non_yearly=True)
-
- skip_row = False
- should_get_last_day = is_last_day_of_the_month(row.depreciation_start_date)
-
- depreciation_amount = 0
-
- number_of_pending_depreciations = final_number_of_depreciations - start
- yearly_opening_wdv = value_after_depreciation
- current_fiscal_year_end_date = None
- prev_per_day_depr = True
- for n in range(start, final_number_of_depreciations):
- # If depreciation is already completed (for double declining balance)
- if skip_row:
- continue
-
- schedule_date = get_last_day(
- add_months(row.depreciation_start_date, n * cint(row.frequency_of_depreciation))
- )
- if not current_fiscal_year_end_date:
- current_fiscal_year_end_date = get_fiscal_year(row.depreciation_start_date)[2]
- elif getdate(schedule_date) > getdate(current_fiscal_year_end_date):
- current_fiscal_year_end_date = add_years(current_fiscal_year_end_date, 1)
- yearly_opening_wdv = value_after_depreciation
-
- if n > 0 and len(self.get("depreciation_schedule")) > n - 1:
- prev_depreciation_amount = self.get("depreciation_schedule")[n - 1].depreciation_amount
- else:
- prev_depreciation_amount = 0
- depreciation_amount, prev_per_day_depr = get_depreciation_amount(
- self,
- asset_doc,
- value_after_depreciation,
- yearly_opening_wdv,
- row,
- n,
- prev_depreciation_amount,
- has_wdv_or_dd_non_yearly_pro_rata,
- number_of_pending_depreciations,
- prev_per_day_depr,
- )
- if not has_pro_rata or (
- n < (cint(final_number_of_depreciations) - 1) or final_number_of_depreciations == 2
- ):
- schedule_date = add_months(
- row.depreciation_start_date, n * cint(row.frequency_of_depreciation)
- )
-
- if should_get_last_day:
- schedule_date = get_last_day(schedule_date)
-
- # if asset is being sold or scrapped
- if date_of_disposal and getdate(schedule_date) >= getdate(date_of_disposal):
- from_date = add_months(
- getdate(asset_doc.available_for_use_date),
- (asset_doc.opening_number_of_booked_depreciations * row.frequency_of_depreciation),
- )
- if is_last_day_of_the_month(getdate(asset_doc.available_for_use_date)):
- from_date = get_last_day(from_date)
- if self.depreciation_schedule:
- from_date = add_days(self.depreciation_schedule[-1].schedule_date, 1)
-
- depreciation_amount, days, months = _get_pro_rata_amt(
- row,
- depreciation_amount,
- from_date,
- date_of_disposal,
- original_schedule_date=schedule_date,
- )
- depreciation_amount = flt(depreciation_amount, asset_doc.precision("gross_purchase_amount"))
- if depreciation_amount > 0:
- self.add_depr_schedule_row(date_of_disposal, depreciation_amount, n)
-
- break
-
- # For first row
- if (
- n == 0
- and (has_pro_rata or has_wdv_or_dd_non_yearly_pro_rata)
- and not self.opening_accumulated_depreciation
- and not self.flags.wdv_it_act_applied
- ):
- from_date = asset_doc.available_for_use_date
- # needed to calc depr amount for available_for_use_date too
- depreciation_amount, days, months = _get_pro_rata_amt(
- row,
- depreciation_amount,
- from_date,
- row.depreciation_start_date,
- has_wdv_or_dd_non_yearly_pro_rata,
- )
- if flt(depreciation_amount, asset_doc.precision("gross_purchase_amount")) <= 0:
- frappe.throw(
- _(
- "Gross Purchase Amount Too Low: {0} cannot be depreciated over {1} cycles with a frequency of {2} depreciations."
- ).format(
- frappe.bold(asset_doc.gross_purchase_amount),
- frappe.bold(row.total_number_of_depreciations),
- frappe.bold(row.frequency_of_depreciation),
- )
- )
- elif n == 0 and has_wdv_or_dd_non_yearly_pro_rata and self.opening_accumulated_depreciation:
- if not is_first_day_of_the_month(getdate(asset_doc.available_for_use_date)):
- from_date = get_last_day(
- add_months(
- getdate(asset_doc.available_for_use_date),
- (
- (self.opening_number_of_booked_depreciations - 1)
- * row.frequency_of_depreciation
- ),
- )
- )
- else:
- from_date = add_months(
- getdate(add_days(asset_doc.available_for_use_date, -1)),
- (self.opening_number_of_booked_depreciations * row.frequency_of_depreciation),
- )
- depreciation_amount, days, months = _get_pro_rata_amt(
- row,
- depreciation_amount,
- from_date,
- row.depreciation_start_date,
- has_wdv_or_dd_non_yearly_pro_rata,
- )
-
- # For last row
- elif has_pro_rata and n == cint(final_number_of_depreciations) - 1:
- if not asset_doc.flags.increase_in_asset_life:
- # In case of increase_in_asset_life, the asset.to_date is already set on asset_repair submission
- asset_doc.to_date = add_months(
- asset_doc.available_for_use_date,
- (n + self.opening_number_of_booked_depreciations)
- * cint(row.frequency_of_depreciation),
- )
- if is_last_day_of_the_month(getdate(asset_doc.available_for_use_date)):
- asset_doc.to_date = get_last_day(asset_doc.to_date)
-
- depreciation_amount_without_pro_rata = depreciation_amount
-
- depreciation_amount, days, months = _get_pro_rata_amt(
- row,
- depreciation_amount,
- schedule_date,
- asset_doc.to_date,
- has_wdv_or_dd_non_yearly_pro_rata,
- )
-
- depreciation_amount = self.get_adjusted_depreciation_amount(
- depreciation_amount_without_pro_rata, depreciation_amount
- )
-
- schedule_date = add_days(schedule_date, days - 1)
-
- if not depreciation_amount:
- continue
- depreciation_amount = flt(depreciation_amount, asset_doc.precision("gross_purchase_amount"))
- value_after_depreciation = flt(
- value_after_depreciation - flt(depreciation_amount),
- asset_doc.precision("gross_purchase_amount"),
- )
-
- # Adjust depreciation amount in the last period based on the expected value after useful life
- if (
- n == cint(final_number_of_depreciations) - 1
- and flt(value_after_depreciation) != flt(row.expected_value_after_useful_life)
- ) or flt(value_after_depreciation) < flt(row.expected_value_after_useful_life):
- depreciation_amount += flt(value_after_depreciation) - flt(
- row.expected_value_after_useful_life
- )
- depreciation_amount = flt(depreciation_amount, asset_doc.precision("gross_purchase_amount"))
- skip_row = True
-
- if flt(depreciation_amount, asset_doc.precision("gross_purchase_amount")) > 0:
- self.add_depr_schedule_row(schedule_date, depreciation_amount, n)
-
- # to ensure that final accumulated depreciation amount is accurate
- def get_adjusted_depreciation_amount(
- self, depreciation_amount_without_pro_rata, depreciation_amount_for_last_row
- ):
- if not self.opening_accumulated_depreciation:
- depreciation_amount_for_first_row = self.get_depreciation_amount_for_first_row()
-
- if (
- depreciation_amount_for_first_row + depreciation_amount_for_last_row
- != depreciation_amount_without_pro_rata
- ):
- depreciation_amount_for_last_row = (
- depreciation_amount_without_pro_rata - depreciation_amount_for_first_row
- )
-
- return depreciation_amount_for_last_row
-
- def get_depreciation_amount_for_first_row(self):
- return self.get("depreciation_schedule")[0].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,
- },
- )
-
- def set_accumulated_depreciation(
- self,
- asset_doc,
- row,
- date_of_disposal=None,
- date_of_return=None,
- ignore_booked_entry=False,
- ):
- straight_line_idx = [
- d.idx
- for d in self.get("depreciation_schedule")
- if self.depreciation_method == "Straight Line" or self.depreciation_method == "Manual"
- ]
-
- accumulated_depreciation = None
- value_after_depreciation = flt(row.value_after_depreciation)
-
- for i, d in enumerate(self.get("depreciation_schedule")):
- if ignore_booked_entry and d.journal_entry:
- continue
-
- if not accumulated_depreciation:
- if i > 0 and (
- asset_doc.flags.decrease_in_asset_value_due_to_value_adjustment
- or asset_doc.flags.increase_in_asset_value_due_to_repair
- ):
- accumulated_depreciation = self.get("depreciation_schedule")[
- i - 1
- ].accumulated_depreciation_amount
- else:
- accumulated_depreciation = flt(
- self.opening_accumulated_depreciation,
- asset_doc.precision("opening_accumulated_depreciation"),
- )
-
- value_after_depreciation -= flt(d.depreciation_amount)
- value_after_depreciation = flt(value_after_depreciation, d.precision("depreciation_amount"))
-
- # for the last row, if depreciation method = Straight Line
- if (
- straight_line_idx
- and i == max(straight_line_idx) - 1
- and not date_of_disposal
- and not date_of_return
- and not row.shift_based
- ):
- d.depreciation_amount += flt(
- value_after_depreciation - flt(row.expected_value_after_useful_life),
- d.precision("depreciation_amount"),
- )
-
- accumulated_depreciation += d.depreciation_amount
- d.accumulated_depreciation_amount = flt(
- accumulated_depreciation, d.precision("accumulated_depreciation_amount")
- )
-
-
-def _get_value_after_depreciation_for_making_schedule(asset_doc, fb_row):
- if asset_doc.docstatus == 1 and fb_row.value_after_depreciation:
- value_after_depreciation = flt(fb_row.value_after_depreciation)
- else:
- value_after_depreciation = flt(asset_doc.gross_purchase_amount) - flt(
- asset_doc.opening_accumulated_depreciation
- )
-
- return value_after_depreciation
-
-
-# if it returns True, depreciation_amount will not be equal for the first and last rows
-def _check_is_pro_rata(asset_doc, row, wdv_or_dd_non_yearly=False):
- has_pro_rata = False
-
- # if not existing asset, from_date = available_for_use_date
- # otherwise, if opening_number_of_booked_depreciations = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12
- # from_date = 01/01/2022
- if row.depreciation_method in ("Straight Line", "Manual"):
- prev_depreciation_start_date = get_last_day(
- add_months(
- row.depreciation_start_date,
- (row.frequency_of_depreciation * -1) * asset_doc.opening_number_of_booked_depreciations,
- )
- )
- from_date = asset_doc.available_for_use_date
- days = date_diff(prev_depreciation_start_date, from_date) + 1
- total_days = get_total_days(prev_depreciation_start_date, row.frequency_of_depreciation)
- else:
- from_date = _get_modified_available_for_use_date(asset_doc, row, wdv_or_dd_non_yearly=False)
- days = date_diff(row.depreciation_start_date, from_date) + 1
- total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation)
- if days <= 0:
- frappe.throw(
- _(
- """Error: This asset already has {0} depreciation periods booked.
- The `depreciation start` date must be at least {1} periods after the `available for use` date.
- Please correct the dates accordingly."""
- ).format(
- asset_doc.opening_number_of_booked_depreciations,
- asset_doc.opening_number_of_booked_depreciations,
- )
- )
- if days < total_days:
- has_pro_rata = True
- return has_pro_rata
-
-
-def _get_modified_available_for_use_date(asset_doc, row, wdv_or_dd_non_yearly=False):
- """
- if Asset has opening booked depreciations = 9,
- available for use date = 17-07-2023,
- depreciation start date = 30-04-2024
- then from date should be 01-04-2024
- """
- if asset_doc.opening_number_of_booked_depreciations > 0:
- from_date = add_months(
- asset_doc.available_for_use_date,
- (asset_doc.opening_number_of_booked_depreciations * row.frequency_of_depreciation) - 1,
- )
- if is_last_day_of_the_month(row.depreciation_start_date):
- return add_days(get_last_day(from_date), 1)
-
- # get from date when depreciation start date is not last day of the month
- months_difference = month_diff(row.depreciation_start_date, from_date) - 1
- return add_days(add_months(row.depreciation_start_date, -1 * months_difference), 1)
- else:
- return asset_doc.available_for_use_date
-
-
-def _get_pro_rata_amt(
- row,
- depreciation_amount,
- from_date,
- to_date,
- has_wdv_or_dd_non_yearly_pro_rata=False,
- original_schedule_date=None,
-):
- days = date_diff(to_date, from_date) + 1
- months = month_diff(to_date, from_date)
- if has_wdv_or_dd_non_yearly_pro_rata:
- total_days = get_total_days(original_schedule_date or to_date, 12)
- else:
- total_days = get_total_days(original_schedule_date or to_date, row.frequency_of_depreciation)
- return (depreciation_amount * flt(days)) / flt(total_days), days, months
-
-
-def get_total_days(date, frequency):
- period_start_date = add_months(date, cint(frequency) * -1)
- if is_last_day_of_the_month(date):
- period_start_date = get_last_day(period_start_date)
- return date_diff(date, period_start_date)
-
-
-def get_depreciation_amount(
- asset_depr_schedule,
- asset,
- depreciable_value,
- yearly_opening_wdv,
- fb_row,
- schedule_idx=0,
- prev_depreciation_amount=0,
- has_wdv_or_dd_non_yearly_pro_rata=False,
- number_of_pending_depreciations=0,
- prev_per_day_depr=0,
-):
- if fb_row.depreciation_method in ("Straight Line", "Manual"):
- return get_straight_line_or_manual_depr_amount(
- asset_depr_schedule, asset, fb_row, schedule_idx, number_of_pending_depreciations
- ), None
- else:
- return get_wdv_or_dd_depr_amount(
- asset,
- fb_row,
- depreciable_value,
- yearly_opening_wdv,
- schedule_idx,
- prev_depreciation_amount,
- has_wdv_or_dd_non_yearly_pro_rata,
- asset_depr_schedule,
- prev_per_day_depr,
- )
-
-
-def get_straight_line_or_manual_depr_amount(
- asset_depr_schedule, asset, 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)) / (
- date_diff(asset.to_date, asset.available_for_use_date) / 365
- )
- # if the Depreciation Schedule is being modified after Asset Repair due to increase in asset value
- elif asset.flags.increase_in_asset_value_due_to_repair:
- return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / flt(
- number_of_pending_depreciations
- )
- # if the Depreciation Schedule is being modified after Asset Value Adjustment due to decrease in asset value
- elif asset.flags.decrease_in_asset_value_due_to_value_adjustment:
- if row.daily_prorata_based:
- amount = flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
-
- return get_daily_prorata_based_straight_line_depr(
- asset,
- row,
- schedule_idx,
- number_of_pending_depreciations,
- amount,
- )
- else:
- return (
- flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
- ) / number_of_pending_depreciations
- # if the Depreciation Schedule is being prepared for the first time
- else:
- if row.daily_prorata_based:
- amount = flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life)
- return get_daily_prorata_based_straight_line_depr(
- asset, row, schedule_idx, number_of_pending_depreciations, amount
- )
- else:
- depreciation_amount = (
- flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life)
- ) / flt(row.total_number_of_depreciations)
- return depreciation_amount
-
-
-def get_daily_prorata_based_straight_line_depr(
- asset, row, schedule_idx, number_of_pending_depreciations, amount
-):
- daily_depr_amount = get_daily_depr_amount(asset, row, schedule_idx, amount)
-
- from_date, total_depreciable_days = _get_total_days(
- row.depreciation_start_date, schedule_idx, row.frequency_of_depreciation
- )
- return daily_depr_amount * total_depreciable_days
-
-
-def get_daily_depr_amount(asset, row, schedule_idx, amount):
- if cint(frappe.db.get_single_value("Accounts Settings", "calculate_depr_using_total_days")):
- total_days = (
- date_diff(
- get_last_day(
- add_months(
- row.depreciation_start_date,
- flt(
- row.total_number_of_depreciations
- - asset.opening_number_of_booked_depreciations
- - 1
- )
- * row.frequency_of_depreciation,
- )
- ),
- add_days(
- get_last_day(
- add_months(
- row.depreciation_start_date,
- (
- row.frequency_of_depreciation
- * (asset.opening_number_of_booked_depreciations + 1)
- )
- * -1,
- ),
- ),
- 1,
- ),
- )
- + 1
- )
-
- return amount / total_days
- else:
- total_years = (
- flt(
- (row.total_number_of_depreciations - row.total_number_of_booked_depreciations)
- * row.frequency_of_depreciation
- )
- / 12
- )
-
- every_year_depr = amount / total_years
-
- depr_period_start_date = add_days(
- get_last_day(add_months(row.depreciation_start_date, row.frequency_of_depreciation * -1)), 1
- )
-
- year_start_date = add_years(
- depr_period_start_date, ((row.frequency_of_depreciation * schedule_idx) // 12)
- )
- year_end_date = add_days(add_years(year_start_date, 1), -1)
-
- return every_year_depr / (date_diff(year_end_date, year_start_date) + 1)
-
-
-def get_shift_depr_amount(asset_depr_schedule, asset, 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.opening_number_of_booked_depreciations)
-
- asset_shift_factors_map = get_asset_shift_factors_map()
- shift = (
- asset_depr_schedule.schedules_before_clearing[schedule_idx].shift
- if len(asset_depr_schedule.schedules_before_clearing) > schedule_idx
- else None
- )
- shift_factor = asset_shift_factors_map.get(shift) if shift else 0
-
- shift_factors_sum = sum(
- flt(asset_shift_factors_map.get(schedule.shift))
- for schedule in asset_depr_schedule.schedules_before_clearing
- )
-
- return (
- (
- flt(asset.gross_purchase_amount)
- - flt(asset.opening_accumulated_depreciation)
- - flt(row.expected_value_after_useful_life)
- )
- / flt(shift_factors_sum)
- ) * shift_factor
-
-
-def get_asset_shift_factors_map():
- return dict(frappe.db.get_all("Asset Shift Factor", ["shift_name", "shift_factor"], as_list=True))
-
-
-@erpnext.allow_regional
-def get_wdv_or_dd_depr_amount(
- asset,
- fb_row,
- depreciable_value,
- yearly_opening_wdv,
- schedule_idx,
- prev_depreciation_amount,
- has_wdv_or_dd_non_yearly_pro_rata,
- asset_depr_schedule,
- prev_per_day_depr,
-):
- return get_default_wdv_or_dd_depr_amount(
- asset,
- fb_row,
- depreciable_value,
- schedule_idx,
- prev_depreciation_amount,
- has_wdv_or_dd_non_yearly_pro_rata,
- asset_depr_schedule,
- prev_per_day_depr,
- )
-
-
-def get_default_wdv_or_dd_depr_amount(
- asset,
- fb_row,
- depreciable_value,
- schedule_idx,
- prev_depreciation_amount,
- has_wdv_or_dd_non_yearly_pro_rata,
- asset_depr_schedule,
- prev_per_day_depr,
-):
- if not fb_row.daily_prorata_based or cint(fb_row.frequency_of_depreciation) == 12:
- return _get_default_wdv_or_dd_depr_amount(
- asset,
- fb_row,
- depreciable_value,
- schedule_idx,
- prev_depreciation_amount,
- has_wdv_or_dd_non_yearly_pro_rata,
- asset_depr_schedule,
- ), None
- else:
- return _get_daily_prorata_based_default_wdv_or_dd_depr_amount(
- asset,
- fb_row,
- depreciable_value,
- schedule_idx,
- prev_depreciation_amount,
- has_wdv_or_dd_non_yearly_pro_rata,
- asset_depr_schedule,
- prev_per_day_depr,
- )
-
-
-def _get_default_wdv_or_dd_depr_amount(
- asset,
- fb_row,
- depreciable_value,
- schedule_idx,
- prev_depreciation_amount,
- has_wdv_or_dd_non_yearly_pro_rata,
- asset_depr_schedule,
-):
- if cint(fb_row.frequency_of_depreciation) == 12:
- return flt(depreciable_value) * (flt(fb_row.rate_of_depreciation) / 100)
- else:
- if has_wdv_or_dd_non_yearly_pro_rata:
- if schedule_idx == 0:
- return flt(depreciable_value) * (flt(fb_row.rate_of_depreciation) / 100)
- elif schedule_idx % (12 / cint(fb_row.frequency_of_depreciation)) == 1:
- return (
- flt(depreciable_value)
- * flt(fb_row.frequency_of_depreciation)
- * (flt(fb_row.rate_of_depreciation) / 1200)
- )
- else:
- return prev_depreciation_amount
- else:
- if schedule_idx % (12 / cint(fb_row.frequency_of_depreciation)) == 0:
- return (
- flt(depreciable_value)
- * flt(fb_row.frequency_of_depreciation)
- * (flt(fb_row.rate_of_depreciation) / 1200)
- )
- else:
- return prev_depreciation_amount
-
-
-def _get_daily_prorata_based_default_wdv_or_dd_depr_amount(
- asset,
- fb_row,
- depreciable_value,
- schedule_idx,
- prev_depreciation_amount,
- has_wdv_or_dd_non_yearly_pro_rata,
- asset_depr_schedule,
- prev_per_day_depr,
-):
- if has_wdv_or_dd_non_yearly_pro_rata: # If applicable days for ther first month is less than full month
- if schedule_idx == 0:
- return flt(depreciable_value) * (flt(fb_row.rate_of_depreciation) / 100), None
-
- elif schedule_idx % (12 / cint(fb_row.frequency_of_depreciation)) == 1: # Year changes
- return get_monthly_depr_amount(fb_row, schedule_idx, depreciable_value)
- else:
- return get_monthly_depr_amount_based_on_prev_per_day_depr(fb_row, schedule_idx, prev_per_day_depr)
- else:
- if schedule_idx % (12 / cint(fb_row.frequency_of_depreciation)) == 0: # year changes
- return get_monthly_depr_amount(fb_row, schedule_idx, depreciable_value)
- else:
- return get_monthly_depr_amount_based_on_prev_per_day_depr(fb_row, schedule_idx, prev_per_day_depr)
-
-
-def get_monthly_depr_amount(fb_row, schedule_idx, depreciable_value):
- """
- Returns monthly depreciation amount when year changes
- 1. Calculate per day depr based on new year
- 2. Calculate monthly amount based on new per day amount
- """
- from_date, days_in_month = _get_total_days(
- fb_row.depreciation_start_date, schedule_idx, cint(fb_row.frequency_of_depreciation)
- )
- per_day_depr = get_per_day_depr(fb_row, depreciable_value, from_date)
- return (per_day_depr * days_in_month), per_day_depr
-
-
-def get_monthly_depr_amount_based_on_prev_per_day_depr(fb_row, schedule_idx, prev_per_day_depr):
- """ "
- Returns monthly depreciation amount based on prev per day depr
- Calculate per day depr only for the first month
- """
- from_date, days_in_month = _get_total_days(
- fb_row.depreciation_start_date, schedule_idx, cint(fb_row.frequency_of_depreciation)
- )
- return (prev_per_day_depr * days_in_month), prev_per_day_depr
-
-
-def get_per_day_depr(
- fb_row,
- depreciable_value,
- from_date,
-):
- to_date = add_days(add_years(from_date, 1), -1)
- total_days = date_diff(to_date, from_date) + 1
- per_day_depr = (flt(depreciable_value) * (flt(fb_row.rate_of_depreciation) / 100)) / total_days
- return per_day_depr
-
-
-def _get_total_days(depreciation_start_date, schedule_idx, frequency_of_depreciation):
- from_date = add_months(depreciation_start_date, (schedule_idx - 1) * frequency_of_depreciation)
- to_date = add_months(from_date, frequency_of_depreciation)
- if is_last_day_of_the_month(depreciation_start_date):
- to_date = get_last_day(to_date)
- from_date = add_days(get_last_day(from_date), 1)
- return from_date, date_diff(to_date, from_date) + 1
-
-
-def make_draft_asset_depr_schedules_if_not_present(asset_doc):
- asset_depr_schedules_names = []
-
- for row in asset_doc.get("finance_books"):
- asset_depr_schedule = get_asset_depr_schedule_name(
- asset_doc.name, ["Draft", "Active"], row.finance_book
- )
-
- if not asset_depr_schedule:
- name = make_draft_asset_depr_schedule(asset_doc, row)
- asset_depr_schedules_names.append(name)
-
- return asset_depr_schedules_names
-
-
-def make_draft_asset_depr_schedules(asset_doc):
- asset_depr_schedules_names = []
-
- for row in asset_doc.get("finance_books"):
- name = make_draft_asset_depr_schedule(asset_doc, row)
- asset_depr_schedules_names.append(name)
-
- return asset_depr_schedules_names
-
def make_draft_asset_depr_schedule(asset_doc, row):
asset_depr_schedule_doc = frappe.new_doc("Asset Depreciation Schedule")
- asset_depr_schedule_doc.prepare_draft_asset_depr_schedule_data(asset_doc, row)
+ asset_depr_schedule_doc.create_depreciation_schedule(asset_doc, row)
asset_depr_schedule_doc.insert()
return asset_depr_schedule_doc.name
-def update_draft_asset_depr_schedules(asset_doc):
- for row in asset_doc.get("finance_books"):
- asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset_doc.name, "Draft", row.finance_book)
-
- if not asset_depr_schedule_doc:
- continue
-
- asset_depr_schedule_doc.prepare_draft_asset_depr_schedule_data(asset_doc, row)
-
- asset_depr_schedule_doc.save()
-
-
def convert_draft_asset_depr_schedules_into_active(asset_doc):
for row in asset_doc.get("finance_books"):
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset_doc.name, "Draft", row.finance_book)
@@ -1046,99 +168,84 @@ def cancel_asset_depr_schedules(asset_doc):
asset_depr_schedule_doc.cancel()
-def make_new_active_asset_depr_schedules_and_cancel_current_ones(
- asset_doc,
- notes,
- date_of_disposal=None,
- date_of_return=None,
- value_after_depreciation=None,
- ignore_booked_entry=False,
- difference_amount=None,
-):
+def reschedule_depreciation(asset_doc, notes, disposal_date=None):
for row in asset_doc.get("finance_books"):
- current_asset_depr_schedule_doc = get_asset_depr_schedule_doc(
- asset_doc.name, "Active", row.finance_book
+ current_schedule = get_asset_depr_schedule_doc(asset_doc.name, None, row.finance_book)
+
+ if current_schedule:
+ if current_schedule.docstatus == 1:
+ new_schedule = frappe.copy_doc(current_schedule)
+ elif current_schedule.docstatus == 0:
+ new_schedule = current_schedule
+ else:
+ new_schedule = frappe.new_doc("Asset Depreciation Schedule")
+ new_schedule.asset = asset_doc.name
+
+ set_modified_depreciation_rate(asset_doc, row, new_schedule)
+
+ new_schedule.create_depreciation_schedule(row, disposal_date)
+ new_schedule.notes = notes
+
+ if current_schedule and current_schedule.docstatus == 1:
+ current_schedule.flags.should_not_cancel_depreciation_entries = True
+ current_schedule.cancel()
+
+ new_schedule.submit()
+
+
+def set_modified_depreciation_rate(asset_doc, row, new_schedule):
+ if row.depreciation_method in (
+ "Written Down Value",
+ "Double Declining Balance",
+ ):
+ new_rate_of_depreciation = flt(
+ asset_doc.get_depreciation_rate(row), row.precision("rate_of_depreciation")
)
- if not current_asset_depr_schedule_doc:
- frappe.throw(
- _("Asset Depreciation Schedule not found for Asset {0} and Finance Book {1}").format(
- get_link_to_form("Asset", asset_doc.name), row.finance_book
- )
- )
-
- new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc)
- if asset_doc.flags.decrease_in_asset_value_due_to_value_adjustment and not value_after_depreciation:
- value_after_depreciation = row.value_after_depreciation - difference_amount
-
- if asset_doc.flags.increase_in_asset_value_due_to_repair and row.depreciation_method in (
- "Written Down Value",
- "Double Declining Balance",
- ):
- new_rate_of_depreciation = flt(
- asset_doc.get_depreciation_rate(row), row.precision("rate_of_depreciation")
- )
- row.rate_of_depreciation = new_rate_of_depreciation
- new_asset_depr_schedule_doc.rate_of_depreciation = new_rate_of_depreciation
-
- new_asset_depr_schedule_doc.make_depr_schedule(
- asset_doc, row, date_of_disposal, value_after_depreciation=value_after_depreciation
- )
- new_asset_depr_schedule_doc.set_accumulated_depreciation(
- asset_doc, row, date_of_disposal, date_of_return, ignore_booked_entry
- )
-
- new_asset_depr_schedule_doc.notes = notes
-
- current_asset_depr_schedule_doc.flags.should_not_cancel_depreciation_entries = True
- current_asset_depr_schedule_doc.cancel()
-
- new_asset_depr_schedule_doc.submit()
+ row.db_set("rate_of_depreciation", new_rate_of_depreciation)
+ new_schedule.rate_of_depreciation = new_rate_of_depreciation
-def get_temp_asset_depr_schedule_doc(
- 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)
+def get_temp_depr_schedule_doc(asset_doc, fb_row, disposal_date=None, updated_depr_schedule=None):
+ current_schedule = get_current_asset_depr(asset_doc, fb_row)
+ temp_schedule_doc = frappe.copy_doc(current_schedule)
- if not current_asset_depr_schedule_doc:
+ if updated_depr_schedule:
+ modify_depreciation_dchedule(temp_schedule_doc, updated_depr_schedule)
+
+ temp_schedule_doc.create_depreciation_schedule(fb_row, disposal_date)
+ return temp_schedule_doc
+
+
+def get_current_asset_depr(asset_doc, row):
+ current_schedule = get_asset_depr_schedule_doc(asset_doc.name, "Active", row.finance_book)
+ if not current_schedule:
frappe.throw(
_("Asset Depreciation Schedule not found for Asset {0} and Finance Book {1}").format(
get_link_to_form("Asset", asset_doc.name), row.finance_book
)
)
+ return current_schedule
- temp_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc)
- if new_depr_schedule:
- temp_asset_depr_schedule_doc.depreciation_schedule = []
+def modify_depreciation_dchedule(temp_schedule_doc, updated_depr_schedule):
+ temp_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,
- },
- )
+ for schedule in updated_depr_schedule:
+ temp_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,
- date_of_disposal,
- date_of_return,
- update_asset_finance_book_row,
- )
- return temp_asset_depr_schedule_doc
+def get_asset_shift_factors_map():
+ return dict(frappe.db.get_all("Asset Shift Factor", ["shift_name", "shift_factor"], as_list=True))
@frappe.whitelist()
@@ -1152,7 +259,7 @@ def get_depr_schedule(asset_name, status, finance_book=None):
@frappe.whitelist()
-def get_asset_depr_schedule_doc(asset_name, status, finance_book=None):
+def get_asset_depr_schedule_doc(asset_name, status=None, finance_book=None):
asset_depr_schedule = get_asset_depr_schedule_name(asset_name, status, finance_book)
if not asset_depr_schedule:
@@ -1163,16 +270,17 @@ def get_asset_depr_schedule_doc(asset_name, status, finance_book=None):
return asset_depr_schedule_doc
-def get_asset_depr_schedule_name(asset_name, status, finance_book=None):
- if isinstance(status, str):
- status = [status]
-
+def get_asset_depr_schedule_name(asset_name, status=None, finance_book=None):
filters = [
["asset", "=", asset_name],
- ["status", "in", status],
["docstatus", "<", 2],
]
+ if status:
+ if isinstance(status, str):
+ status = [status]
+ filters.append(["status", "in", status])
+
if finance_book:
filters.append(["finance_book", "=", finance_book])
else:
diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/deppreciation_schedule_controller.py b/erpnext/assets/doctype/asset_depreciation_schedule/deppreciation_schedule_controller.py
new file mode 100644
index 00000000000..c228dfbb8c7
--- /dev/null
+++ b/erpnext/assets/doctype/asset_depreciation_schedule/deppreciation_schedule_controller.py
@@ -0,0 +1,459 @@
+import frappe
+from frappe import _
+from frappe.utils import (
+ add_days,
+ add_months,
+ add_years,
+ cint,
+ date_diff,
+ flt,
+ get_last_day,
+ getdate,
+ is_last_day_of_the_month,
+ month_diff,
+ nowdate,
+)
+
+import erpnext
+from erpnext.accounts.utils import get_fiscal_year
+from erpnext.assets.doctype.asset_depreciation_schedule.depreciation_methods import (
+ StraightLineMethod,
+ WDVMethod,
+)
+
+
+class DepreciationScheduleController(StraightLineMethod, WDVMethod):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ def create_depreciation_schedule(self, fb_row=None, disposal_date=None):
+ self.disposal_date = disposal_date
+ self.asset_doc = frappe.get_doc("Asset", self.asset)
+
+ self.get_finance_book_row(fb_row)
+ self.fetch_asset_details()
+ self.clear()
+ self.create()
+ self.set_accumulated_depreciation()
+
+ def clear(self):
+ self.first_non_depreciated_row_idx = 0
+ num_of_depreciations_completed = 0
+ depr_schedule = []
+
+ self.schedules_before_clearing = self.get("depreciation_schedule")
+ for schedule in self.get("depreciation_schedule"):
+ if schedule.journal_entry:
+ num_of_depreciations_completed += 1
+ depr_schedule.append(schedule)
+ else:
+ self.first_non_depreciated_row_idx = num_of_depreciations_completed
+ break
+
+ self.depreciation_schedule = depr_schedule
+
+ def create(self):
+ self.initialize_variables()
+ for row_idx in range(self.first_non_depreciated_row_idx, self.final_number_of_depreciations):
+ # If depreciation is already completed (for double declining balance)
+ if self.skip_row:
+ continue
+
+ self.has_fiscal_year_changed(row_idx)
+ if self.fiscal_year_changed:
+ self.yearly_opening_wdv = self.pending_depreciation_amount
+
+ self.get_prev_depreciation_amount(row_idx)
+
+ self.schedule_date = self.get_next_schedule_date(row_idx)
+
+ self.depreciation_amount = self.get_depreciation_amount(row_idx)
+
+ # if asset is being sold or scrapped
+ if self.disposal_date and getdate(self.schedule_date) >= getdate(self.disposal_date):
+ self.set_depreciation_amount_for_disposal(row_idx)
+ break
+
+ if row_idx == 0:
+ self.set_depreciation_amount_for_first_row(row_idx)
+ elif (
+ self.has_pro_rata and row_idx == cint(self.final_number_of_depreciations) - 1
+ ): # for the last row
+ self.set_depreciation_amount_for_last_row(row_idx)
+
+ self.depreciation_amount = flt(
+ self.depreciation_amount, self.asset_doc.precision("gross_purchase_amount")
+ )
+ if not self.depreciation_amount:
+ break
+
+ self.pending_depreciation_amount = flt(
+ self.pending_depreciation_amount - self.depreciation_amount,
+ self.asset_doc.precision("gross_purchase_amount"),
+ )
+
+ self.adjust_depr_amount_for_salvage_value(row_idx)
+
+ if flt(self.depreciation_amount, self.asset_doc.precision("gross_purchase_amount")) > 0:
+ self.add_depr_schedule_row(row_idx)
+
+ def initialize_variables(self):
+ self.pending_depreciation_amount = self.fb_row.value_after_depreciation
+ self.should_get_last_day = is_last_day_of_the_month(self.fb_row.depreciation_start_date)
+ self.skip_row = False
+ self.depreciation_amount = 0
+ self.prev_per_day_depr = True
+ self.prev_depreciation_amount = 0
+ self.current_fiscal_year_end_date = None
+ self.yearly_opening_wdv = self.pending_depreciation_amount
+ self.get_number_of_pending_months()
+ self.get_final_number_of_depreciations()
+ self.is_wdv_or_dd_non_yearly_pro_rata()
+ self.get_total_pending_days_or_years()
+
+ def get_final_number_of_depreciations(self):
+ self.final_number_of_depreciations = cint(self.fb_row.total_number_of_depreciations) - cint(
+ self.opening_number_of_booked_depreciations
+ )
+
+ self._check_is_pro_rata()
+ if self.has_pro_rata:
+ self.final_number_of_depreciations += 1
+
+ self.set_final_number_of_depreciations_considering_increase_in_asset_life()
+
+ def set_final_number_of_depreciations_considering_increase_in_asset_life(self):
+ # final schedule date after increasing asset life
+ self.final_schedule_date = add_months(
+ self.asset_doc.available_for_use_date,
+ (self.fb_row.total_number_of_depreciations * cint(self.fb_row.frequency_of_depreciation))
+ + cint(self.fb_row.increase_in_asset_life),
+ )
+
+ number_of_pending_depreciations = cint(self.fb_row.total_number_of_depreciations) - cint(
+ self.asset_doc.opening_number_of_booked_depreciations
+ )
+ schedule_date = add_months(
+ self.fb_row.depreciation_start_date,
+ number_of_pending_depreciations * cint(self.fb_row.frequency_of_depreciation),
+ )
+
+ if self.final_schedule_date > getdate(schedule_date):
+ months = month_diff(self.final_schedule_date, schedule_date)
+ self.final_number_of_depreciations += months // cint(self.fb_row.frequency_of_depreciation) + 1
+
+ def is_wdv_or_dd_non_yearly_pro_rata(self):
+ if (
+ self.fb_row.depreciation_method in ("Written Down Value", "Double Declining Balance")
+ and cint(self.fb_row.frequency_of_depreciation) != 12
+ ):
+ self._check_is_pro_rata()
+
+ def _check_is_pro_rata(self):
+ self.has_pro_rata = False
+
+ # if not existing asset, from_date = available_for_use_date
+ # otherwise, if opening_number_of_booked_depreciations = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12
+ # from_date = 01/01/2022
+ if self.fb_row.depreciation_method in ("Straight Line", "Manual"):
+ prev_depreciation_start_date = get_last_day(
+ add_months(
+ self.fb_row.depreciation_start_date,
+ (self.fb_row.frequency_of_depreciation * -1)
+ * self.asset_doc.opening_number_of_booked_depreciations,
+ )
+ )
+ from_date = self.asset_doc.available_for_use_date
+ days = date_diff(prev_depreciation_start_date, from_date) + 1
+ total_days = self.get_total_days(prev_depreciation_start_date)
+ else:
+ from_date = self._get_modified_available_for_use_date_for_existing_assets()
+ days = date_diff(self.fb_row.depreciation_start_date, from_date) + 1
+ total_days = self.get_total_days(self.fb_row.depreciation_start_date)
+
+ if days <= 0:
+ frappe.throw(
+ _(
+ """Error: This asset already has {0} depreciation periods booked.
+ The `depreciation start` date must be at least {1} periods after the `available for use` date.
+ Please correct the dates accordingly."""
+ ).format(
+ self.asset_doc.opening_number_of_booked_depreciations,
+ self.asset_doc.opening_number_of_booked_depreciations,
+ )
+ )
+ if days < total_days:
+ self.has_pro_rata = True
+ self.has_wdv_or_dd_non_yearly_pro_rata = True
+
+ def _get_modified_available_for_use_date_for_existing_assets(self):
+ """
+ if Asset has opening booked depreciations = 3,
+ frequency of depreciation = 3,
+ available for use date = 17-07-2023,
+ depreciation start date = 30-06-2024
+ then from date should be 01-04-2024
+ """
+ if self.asset_doc.opening_number_of_booked_depreciations > 0:
+ from_date = add_days(
+ add_months(self.fb_row.depreciation_start_date, (self.fb_row.frequency_of_depreciation * -1)),
+ 1,
+ )
+ return from_date
+ else:
+ return self.asset_doc.available_for_use_date
+
+ def get_total_days(self, date):
+ period_start_date = add_months(date, cint(self.fb_row.frequency_of_depreciation) * -1)
+ if is_last_day_of_the_month(date):
+ period_start_date = get_last_day(period_start_date)
+ return date_diff(date, period_start_date)
+
+ def _get_pro_rata_amt(self, from_date, to_date, original_schedule_date=None):
+ days = date_diff(to_date, from_date) + 1
+ months = month_diff(to_date, from_date)
+ total_days = self.get_total_days(original_schedule_date or to_date)
+ return (self.depreciation_amount * flt(days)) / flt(total_days), days, months
+
+ def get_number_of_pending_months(self):
+ total_months = cint(self.fb_row.total_number_of_depreciations) * cint(
+ self.fb_row.frequency_of_depreciation
+ ) + cint(self.fb_row.increase_in_asset_life)
+ last_depr_date = self.get_last_booked_depreciation_date()
+ depr_booked_for_months = self.get_booked_depr_for_months_count(last_depr_date)
+
+ self.pending_months = total_months - depr_booked_for_months
+
+ def get_last_booked_depreciation_date(self):
+ last_depr_date = None
+ if self.first_non_depreciated_row_idx > 0:
+ last_depr_date = self.depreciation_schedule[self.first_non_depreciated_row_idx - 1].schedule_date
+ elif self.asset_doc.opening_number_of_booked_depreciations > 0:
+ last_depr_date = add_months(
+ self.fb_row.depreciation_start_date, -1 * self.fb_row.frequency_of_depreciation
+ )
+ return last_depr_date
+
+ def get_booked_depr_for_months_count(self, last_depr_date):
+ depr_booked_for_months = 0
+ if last_depr_date:
+ asset_used_for_months = self.fb_row.frequency_of_depreciation * (
+ 1 + self.asset_doc.opening_number_of_booked_depreciations
+ )
+ computed_available_for_use_date = add_days(
+ add_months(self.fb_row.depreciation_start_date, -1 * asset_used_for_months), 1
+ )
+ if getdate(computed_available_for_use_date) < getdate(self.asset_doc.available_for_use_date):
+ computed_available_for_use_date = self.asset_doc.available_for_use_date
+ depr_booked_for_months = (date_diff(last_depr_date, computed_available_for_use_date) + 1) / (
+ 365 / 12
+ )
+ return depr_booked_for_months
+
+ def get_total_pending_days_or_years(self):
+ if cint(frappe.db.get_single_value("Accounts Settings", "calculate_depr_using_total_days")):
+ last_depr_date = self.get_last_booked_depreciation_date()
+ if last_depr_date:
+ self.total_pending_days = date_diff(self.final_schedule_date, last_depr_date) - 1
+ else:
+ self.total_pending_days = date_diff(
+ self.final_schedule_date, self.asset_doc.available_for_use_date
+ )
+ else:
+ self.total_pending_years = self.pending_months / 12
+
+ def has_fiscal_year_changed(self, row_idx):
+ self.fiscal_year_changed = False
+
+ schedule_date = get_last_day(
+ add_months(
+ self.fb_row.depreciation_start_date, row_idx * cint(self.fb_row.frequency_of_depreciation)
+ )
+ )
+
+ if not self.current_fiscal_year_end_date:
+ self.current_fiscal_year_end_date = get_fiscal_year(self.fb_row.depreciation_start_date)[2]
+ self.fiscal_year_changed = True
+ elif getdate(schedule_date) > getdate(self.current_fiscal_year_end_date):
+ self.current_fiscal_year_end_date = add_years(self.current_fiscal_year_end_date, 1)
+ self.fiscal_year_changed = True
+
+ def get_prev_depreciation_amount(self, row_idx):
+ if row_idx > 1:
+ self.prev_depreciation_amount = 0
+ if len(self.get("depreciation_schedule")) > row_idx - 1:
+ self.prev_depreciation_amount = self.get("depreciation_schedule")[
+ row_idx - 1
+ ].depreciation_amount
+
+ def get_next_schedule_date(self, row_idx):
+ schedule_date = add_months(
+ self.fb_row.depreciation_start_date, row_idx * cint(self.fb_row.frequency_of_depreciation)
+ )
+ if self.should_get_last_day:
+ schedule_date = get_last_day(schedule_date)
+
+ return schedule_date
+
+ def set_depreciation_amount_for_disposal(self, row_idx):
+ if self.depreciation_schedule: # if there are already booked depreciations
+ from_date = add_days(self.depreciation_schedule[-1].schedule_date, 1)
+ else:
+ from_date = self._get_modified_available_for_use_date_for_existing_assets()
+ if is_last_day_of_the_month(getdate(self.asset_doc.available_for_use_date)):
+ from_date = get_last_day(from_date)
+
+ self.depreciation_amount, days, months = self._get_pro_rata_amt(
+ from_date,
+ self.disposal_date,
+ original_schedule_date=self.schedule_date,
+ )
+
+ self.depreciation_amount = flt(
+ self.depreciation_amount, self.asset_doc.precision("gross_purchase_amount")
+ )
+ if self.depreciation_amount > 0:
+ self.schedule_date = self.disposal_date
+ self.add_depr_schedule_row(row_idx)
+
+ def set_depreciation_amount_for_first_row(self, row_idx):
+ """
+ For the first row, if available for use date is mid of the month, then pro rata amount is needed
+ """
+ pro_rata_amount_applicable = False
+ if (
+ self.has_pro_rata
+ and not self.opening_accumulated_depreciation
+ and not self.flags.wdv_it_act_applied
+ ): # if not existing asset
+ from_date = self.asset_doc.available_for_use_date
+ pro_rata_amount_applicable = True
+ elif self.has_pro_rata and self.opening_accumulated_depreciation: # if existing asset
+ from_date = self._get_modified_available_for_use_date_for_existing_assets()
+ pro_rata_amount_applicable = True
+
+ if pro_rata_amount_applicable:
+ self.depreciation_amount, days, months = self._get_pro_rata_amt(
+ from_date,
+ self.fb_row.depreciation_start_date,
+ )
+
+ self.validate_depreciation_amount_for_low_value_assets()
+
+ def set_depreciation_amount_for_last_row(self, row_idx):
+ if not self.fb_row.increase_in_asset_life:
+ self.final_schedule_date = add_months(
+ self.asset_doc.available_for_use_date,
+ (row_idx + self.opening_number_of_booked_depreciations)
+ * cint(self.fb_row.frequency_of_depreciation),
+ )
+ if is_last_day_of_the_month(getdate(self.asset_doc.available_for_use_date)):
+ self.final_schedule_date = get_last_day(self.final_schedule_date)
+
+ if self.opening_accumulated_depreciation:
+ self.depreciation_amount, days, months = self._get_pro_rata_amt(
+ self.schedule_date,
+ self.final_schedule_date,
+ )
+ else:
+ if not self.fb_row.increase_in_asset_life:
+ self.depreciation_amount -= self.get("depreciation_schedule")[0].depreciation_amount
+ days = date_diff(self.final_schedule_date, self.schedule_date) + 1
+
+ self.schedule_date = add_days(self.schedule_date, days - 1)
+
+ def adjust_depr_amount_for_salvage_value(self, row_idx):
+ """
+ Adjust depreciation amount in the last period based on the expected value after useful life
+ """
+ if (
+ row_idx == cint(self.final_number_of_depreciations) - 1
+ and flt(self.pending_depreciation_amount) != flt(self.fb_row.expected_value_after_useful_life)
+ ) or flt(self.pending_depreciation_amount) < flt(self.fb_row.expected_value_after_useful_life):
+ self.depreciation_amount += flt(self.pending_depreciation_amount) - flt(
+ self.fb_row.expected_value_after_useful_life
+ )
+ self.depreciation_amount = flt(
+ self.depreciation_amount, self.precision("value_after_depreciation")
+ )
+ self.skip_row = True
+
+ def validate_depreciation_amount_for_low_value_assets(self):
+ """
+ If gross purchase amount is too low, then depreciation amount
+ can come zero sometimes based on the frequency and number of depreciations.
+ """
+ if flt(self.depreciation_amount, self.asset_doc.precision("gross_purchase_amount")) <= 0:
+ frappe.throw(
+ _("Gross Purchase Amount {0} cannot be depreciated over {1} cycles.").format(
+ frappe.bold(self.asset_doc.gross_purchase_amount),
+ frappe.bold(self.fb_row.total_number_of_depreciations),
+ )
+ )
+
+ def add_depr_schedule_row(self, row_idx):
+ shift = None
+ if self.shift_based:
+ shift = (
+ self.schedules_before_clearing[row_idx].shift
+ if (self.schedules_before_clearing and len(self.schedules_before_clearing) > row_idx)
+ else frappe.get_cached_value("Asset Shift Factor", {"default": 1}, "shift_name")
+ )
+
+ self.append(
+ "depreciation_schedule",
+ {
+ "schedule_date": self.schedule_date,
+ "depreciation_amount": self.depreciation_amount,
+ "shift": shift,
+ },
+ )
+
+ def set_accumulated_depreciation(self):
+ accumulated_depreciation = flt(self.opening_accumulated_depreciation)
+ for d in self.get("depreciation_schedule"):
+ if d.journal_entry:
+ accumulated_depreciation = d.accumulated_depreciation_amount
+ continue
+
+ accumulated_depreciation += d.depreciation_amount
+ d.accumulated_depreciation_amount = flt(
+ accumulated_depreciation, d.precision("accumulated_depreciation_amount")
+ )
+
+ def get_depreciation_amount(self, row_idx):
+ if self.fb_row.depreciation_method in ("Straight Line", "Manual"):
+ return self.get_straight_line_depr_amount(row_idx)
+ else:
+ return self.get_wdv_or_dd_depr_amount(row_idx)
+
+ def _get_total_days(self, depreciation_start_date, row_idx):
+ from_date = add_months(depreciation_start_date, (row_idx - 1) * self.frequency_of_depreciation)
+ to_date = add_months(from_date, self.frequency_of_depreciation)
+ if is_last_day_of_the_month(depreciation_start_date):
+ to_date = get_last_day(to_date)
+ from_date = add_days(get_last_day(from_date), 1)
+ return from_date, date_diff(to_date, from_date) + 1
+
+ def get_total_days_in_current_depr_year(self):
+ fy_start_date, fy_end_date = self.get_fiscal_year(self.schedule_date)
+ return date_diff(fy_end_date, fy_start_date) + 1
+
+ def get_fiscal_year(self, date):
+ fy = get_fiscal_year(date, as_dict=True, raise_on_missing=False)
+ if fy:
+ fy_start_date = fy.year_start_date
+ fy_end_date = fy.year_end_date
+ else:
+ current_fy = get_fiscal_year(nowdate(), as_dict=True)
+ # get fiscal year start date of the year in which the schedule date falls
+ months = month_diff(date, current_fy.year_start_date)
+ if months % 12:
+ years = months // 12
+ else:
+ years = months // 12 - 1
+
+ fy_start_date = add_years(current_fy.year_start_date, years)
+ fy_end_date = add_days(add_years(fy_start_date, 1), -1)
+
+ return fy_start_date, fy_end_date
diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/depreciation_methods.py b/erpnext/assets/doctype/asset_depreciation_schedule/depreciation_methods.py
new file mode 100644
index 00000000000..1b235b1fa78
--- /dev/null
+++ b/erpnext/assets/doctype/asset_depreciation_schedule/depreciation_methods.py
@@ -0,0 +1,121 @@
+import frappe
+from frappe.model.document import Document
+from frappe.utils import (
+ cint,
+ flt,
+)
+
+import erpnext
+from erpnext.accounts.utils import get_fiscal_year
+
+# from erpnext.assets.doctype.asset_depreciation_schedule.deppreciation_schedule_controller import (
+# _get_total_days,
+# )
+
+
+class StraightLineMethod(Document):
+ def get_straight_line_depr_amount(self, row_idx):
+ self.depreciable_value = flt(self.fb_row.value_after_depreciation) - flt(
+ self.fb_row.expected_value_after_useful_life
+ )
+
+ if self.fb_row.shift_based:
+ return self.get_shift_depr_amount(row_idx)
+
+ if self.fb_row.daily_prorata_based:
+ return self.get_daily_prorata_based_depr_amount(row_idx)
+ else:
+ return self.get_fixed_depr_amount()
+
+ def get_fixed_depr_amount(self):
+ pending_periods = flt(self.pending_months) / flt(self.fb_row.frequency_of_depreciation)
+ return self.depreciable_value / pending_periods
+
+ def get_daily_prorata_based_depr_amount(self, row_idx):
+ daily_depr_amount = self.get_daily_depr_amount()
+
+ from_date, total_depreciable_days = self._get_total_days(self.fb_row.depreciation_start_date, row_idx)
+ return daily_depr_amount * total_depreciable_days
+
+ def get_daily_depr_amount(self):
+ if cint(frappe.db.get_single_value("Accounts Settings", "calculate_depr_using_total_days")):
+ return self.depreciable_value / self.total_pending_days
+ else:
+ yearly_depr_amount = self.depreciable_value / self.total_pending_years
+ total_days_in_current_depr_year = self.get_total_days_in_current_depr_year()
+ return yearly_depr_amount / total_days_in_current_depr_year
+
+ def get_shift_depr_amount(self, row_idx):
+ if not self.schedules_before_clearing:
+ pending_periods = flt(self.pending_months) / flt(self.fb_row.frequency_of_depreciation)
+ return self.depreciable_value / pending_periods
+
+ asset_shift_factors_map = self.get_asset_shift_factors_map()
+
+ if self.schedules_before_clearing:
+ shift = (
+ self.schedules_before_clearing[row_idx].shift
+ if len(self.schedules_before_clearing) > row_idx
+ else None
+ )
+
+ shift_factor = asset_shift_factors_map.get(shift, 0)
+ shift_factors_sum = sum(
+ [
+ flt(asset_shift_factors_map.get(d.shift))
+ for d in self.schedules_before_clearing
+ if not d.journal_entry
+ ]
+ )
+
+ return (self.depreciable_value / shift_factors_sum) * shift_factor
+
+ def get_asset_shift_factors_map(self):
+ return dict(frappe.db.get_all("Asset Shift Factor", ["shift_name", "shift_factor"], as_list=True))
+
+
+class WDVMethod(Document):
+ @erpnext.allow_regional
+ def get_wdv_or_dd_depr_amount(self, row_idx):
+ return WDVMethod.calculate_wdv_or_dd_based_depreciation_amount(self, row_idx)
+
+ @staticmethod
+ def calculate_wdv_or_dd_based_depreciation_amount(self, row_idx):
+ if self.fb_row.daily_prorata_based:
+ return self.get_daily_prorata_based_wdv_depr_amount(row_idx)
+ else:
+ return self.get_wdv_depr_amount()
+
+ def get_wdv_depr_amount(self):
+ if self.is_fiscal_year_changed():
+ yearly_amount = (
+ flt(self.pending_depreciation_amount) * flt(self.fb_row.rate_of_depreciation) / 100
+ )
+
+ depreciation_amount = (yearly_amount * self.fb_row.frequency_of_depreciation) / 12
+ self.prev_depreciation_amount = depreciation_amount
+
+ return depreciation_amount
+ else:
+ return self.prev_depreciation_amount
+
+ def is_fiscal_year_changed(self):
+ fy_start_date, fy_end_date = self.get_fiscal_year(self.schedule_date)
+ if fy_start_date != self.get("prev_fy_start_date"):
+ self.prev_fy_start_date = fy_start_date
+ return True
+
+ def get_daily_prorata_based_wdv_depr_amount(self, row_idx):
+ daily_depr_amount = self.get_daily_wdv_depr_amount()
+
+ from_date, total_depreciable_days = self._get_total_days(self.fb_row.depreciation_start_date, row_idx)
+ return daily_depr_amount * total_depreciable_days
+
+ def get_daily_wdv_depr_amount(self):
+ if self.is_fiscal_year_changed():
+ self.yearly_wdv_depr_amount = (
+ self.pending_depreciation_amount * self.fb_row.rate_of_depreciation / 100
+ )
+
+ total_days_in_current_depr_year = self.get_total_days_in_current_depr_year()
+ return self.yearly_wdv_depr_amount / total_days_in_current_depr_year
diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/test_asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/test_asset_depreciation_schedule.py
index 61e3761719e..e36042bd6e7 100644
--- a/erpnext/assets/doctype/asset_depreciation_schedule/test_asset_depreciation_schedule.py
+++ b/erpnext/assets/doctype/asset_depreciation_schedule/test_asset_depreciation_schedule.py
@@ -3,8 +3,9 @@
import frappe
from frappe.tests import IntegrationTestCase, UnitTestCase
-from frappe.utils import cstr, flt
+from frappe.utils import cstr, date_diff, flt, getdate
+from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.assets.doctype.asset.depreciation import (
post_depreciation_entries,
)
@@ -13,6 +14,10 @@ from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_sched
get_asset_depr_schedule_doc,
get_depr_schedule,
)
+from erpnext.assets.doctype.asset_repair.test_asset_repair import create_asset_repair
+from erpnext.assets.doctype.asset_value_adjustment.test_asset_value_adjustment import (
+ make_asset_value_adjustment,
+)
class UnitTestAssetDepreciationSchedule(UnitTestCase):
@@ -41,6 +46,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
self.assertRaises(frappe.ValidationError, second_asset_depr_schedule.insert)
def test_daily_prorata_based_depr_on_sl_method(self):
+ frappe.db.set_single_value("Accounts Settings", "calculate_depr_using_total_days", 0)
asset = create_asset(
calculate_depreciation=1,
depreciation_method="Straight Line",
@@ -178,15 +184,15 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
)
expected_schedules = [
- ["2024-12-31", 60.92, 284.07],
- ["2025-03-31", 60.92, 344.99],
- ["2025-06-30", 60.92, 405.91],
- ["2025-09-30", 60.92, 466.83],
- ["2025-12-31", 60.92, 527.75],
- ["2026-03-31", 60.92, 588.67],
- ["2026-06-30", 60.92, 649.59],
- ["2026-09-30", 60.92, 710.51],
- ["2026-11-01", 20.49, 731.0],
+ ["2024-12-31", 60.98, 284.13],
+ ["2025-03-31", 60.98, 345.11],
+ ["2025-06-30", 60.98, 406.09],
+ ["2025-09-30", 60.98, 467.07],
+ ["2025-12-31", 60.98, 528.05],
+ ["2026-03-31", 60.98, 589.03],
+ ["2026-06-30", 60.98, 650.01],
+ ["2026-09-30", 60.98, 710.99],
+ ["2026-11-01", 20.01, 731.0],
]
schedules = [
[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount]
@@ -273,12 +279,12 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
expected_schedules = [
["2021-03-31", 4383.56, 4383.56],
- ["2021-06-30", 9535.45, 13919.01],
- ["2021-09-30", 9640.23, 23559.24],
- ["2021-12-31", 9640.23, 33199.47],
- ["2022-03-31", 9430.66, 42630.13],
- ["2022-06-30", 5721.27, 48351.4],
- ["2022-08-20", 51648.6, 100000.0],
+ ["2021-06-30", 9972.6, 14356.16],
+ ["2021-09-30", 10082.19, 24438.35],
+ ["2021-12-31", 10082.19, 34520.54],
+ ["2022-03-31", 6458.25, 40978.79],
+ ["2022-06-30", 6530.01, 47508.8],
+ ["2022-08-20", 52491.2, 100000.0],
]
schedules = [
@@ -302,13 +308,13 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
)
expected_schedules = [
- ["2020-02-29", 1092.90, 1092.90],
- ["2020-08-31", 19944.01, 21036.91],
- ["2021-02-28", 19618.83, 40655.74],
- ["2021-08-31", 11966.4, 52622.14],
- ["2022-02-28", 11771.3, 64393.44],
- ["2022-08-31", 7179.84, 71573.28],
- ["2023-02-20", 28426.72, 100000.0],
+ ["2020-02-29", 1092.9, 1092.9],
+ ["2020-08-31", 20109.29, 21202.19],
+ ["2021-02-28", 15630.03, 36832.22],
+ ["2021-08-31", 15889.09, 52721.31],
+ ["2022-02-28", 9378.02, 62099.33],
+ ["2022-08-31", 9533.46, 71632.79],
+ ["2023-02-20", 28367.21, 100000.0],
]
schedules = [
@@ -377,36 +383,790 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
self.assertEqual(asset.finance_books[0].total_number_of_booked_depreciations, 14)
- def test_schedule_for_wdv_method_for_existing_asset(self):
+ def test_depreciation_schedule_after_cancelling_asset_repair(self):
asset = create_asset(
+ item_code="Macbook Pro",
+ gross_purchase_amount=500,
calculate_depreciation=1,
- depreciation_method="Written Down Value",
- available_for_use_date="2020-07-17",
- is_existing_asset=1,
- opening_number_of_booked_depreciations=2,
- opening_accumulated_depreciation=11666.67,
- depreciation_start_date="2021-04-30",
+ depreciation_method="Straight Line",
+ available_for_use_date="2023-01-01",
+ depreciation_start_date="2023-01-31",
+ frequency_of_depreciation=1,
total_number_of_depreciations=12,
- frequency_of_depreciation=3,
- gross_purchase_amount=50000,
- rate_of_depreciation=40,
+ submit=1,
)
- self.assertEqual(asset.status, "Draft")
- expected_schedules = [
- ["2021-04-30", 3833.33, 15500.0],
- ["2021-07-31", 3833.33, 19333.33],
- ["2021-10-31", 3833.33, 23166.66],
- ["2022-01-31", 3833.33, 26999.99],
- ["2022-04-30", 2300.0, 29299.99],
- ["2022-07-31", 2300.0, 31599.99],
- ["2022-10-31", 2300.0, 33899.99],
- ["2023-01-31", 2300.0, 36199.99],
- ["2023-04-30", 1380.0, 37579.99],
- ["2023-07-31", 12420.01, 50000.0],
+ expected_depreciation_before_repair = [
+ ["2023-01-31", 41.67, 41.67],
+ ["2023-02-28", 41.67, 83.34],
+ ["2023-03-31", 41.67, 125.01],
+ ["2023-04-30", 41.67, 166.68],
+ ["2023-05-31", 41.67, 208.35],
+ ["2023-06-30", 41.67, 250.02],
+ ["2023-07-31", 41.67, 291.69],
+ ["2023-08-31", 41.67, 333.36],
+ ["2023-09-30", 41.67, 375.03],
+ ["2023-10-31", 41.67, 416.7],
+ ["2023-11-30", 41.67, 458.37],
+ ["2023-12-31", 41.63, 500.0],
+ ]
+
+ schedules = [
+ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
+ for d in get_depr_schedule(asset.name, "Active")
+ ]
+ self.assertEqual(schedules, expected_depreciation_before_repair)
+ self.assertEqual(asset.finance_books[0].value_after_depreciation, 500)
+
+ asset_repair = create_asset_repair(
+ asset=asset,
+ capitalize_repair_cost=1,
+ item="_Test Non Stock Item",
+ failure_date="2023-04-01",
+ pi_repair_cost1=60,
+ pi_repair_cost2=40,
+ increase_in_asset_life=0,
+ submit=1,
+ )
+ self.assertEqual(asset_repair.total_repair_cost, 100)
+
+ expected_depreciation_after_repair = [
+ ["2023-01-31", 50.0, 50.0],
+ ["2023-02-28", 50.0, 100.0],
+ ["2023-03-31", 50.0, 150.0],
+ ["2023-04-30", 50.0, 200.0],
+ ["2023-05-31", 50.0, 250.0],
+ ["2023-06-30", 50.0, 300.0],
+ ["2023-07-31", 50.0, 350.0],
+ ["2023-08-31", 50.0, 400.0],
+ ["2023-09-30", 50.0, 450.0],
+ ["2023-10-31", 50.0, 500.0],
+ ["2023-11-30", 50.0, 550.0],
+ ["2023-12-31", 50.0, 600.0],
+ ]
+
+ schedules = [
+ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
+ for d in get_depr_schedule(asset.name, "Active")
+ ]
+ self.assertEqual(schedules, expected_depreciation_after_repair)
+ asset.reload()
+ self.assertEqual(asset.finance_books[0].value_after_depreciation, 600)
+
+ asset_repair.cancel()
+
+ schedules = [
+ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
+ for d in get_depr_schedule(asset.name, "Active")
+ ]
+ self.assertEqual(schedules, expected_depreciation_before_repair)
+ asset.reload()
+ self.assertEqual(asset.finance_books[0].value_after_depreciation, 500)
+
+ def test_depreciation_schedule_after_cancelling_asset_repair_for_6_months_frequency(self):
+ asset = create_asset(
+ item_code="Macbook Pro",
+ gross_purchase_amount=500,
+ calculate_depreciation=1,
+ depreciation_method="Straight Line",
+ available_for_use_date="2023-01-01",
+ depreciation_start_date="2023-06-30",
+ frequency_of_depreciation=6,
+ total_number_of_depreciations=4,
+ submit=1,
+ )
+
+ expected_depreciation_before_repair = [
+ ["2023-06-30", 125.0, 125.0],
+ ["2023-12-31", 125.0, 250.0],
+ ["2024-06-30", 125.0, 375.0],
+ ["2024-12-31", 125.0, 500.0],
+ ]
+
+ schedules = [
+ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
+ for d in get_depr_schedule(asset.name, "Active")
+ ]
+
+ self.assertEqual(schedules, expected_depreciation_before_repair)
+
+ asset_repair = create_asset_repair(
+ asset=asset,
+ capitalize_repair_cost=1,
+ item="_Test Non Stock Item",
+ failure_date="2023-04-01",
+ pi_repair_cost1=60,
+ pi_repair_cost2=40,
+ increase_in_asset_life=0,
+ submit=1,
+ )
+ self.assertEqual(asset_repair.total_repair_cost, 100)
+
+ expected_depreciation_after_repair = [
+ ["2023-06-30", 150.0, 150.0],
+ ["2023-12-31", 150.0, 300.0],
+ ["2024-06-30", 150.0, 450.0],
+ ["2024-12-31", 150.0, 600.0],
+ ]
+
+ schedules = [
+ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
+ for d in get_depr_schedule(asset.name, "Active")
+ ]
+
+ self.assertEqual(schedules, expected_depreciation_after_repair)
+ asset.reload()
+ self.assertEqual(asset.finance_books[0].value_after_depreciation, 600)
+
+ asset_repair.cancel()
+
+ schedules = [
+ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
+ for d in get_depr_schedule(asset.name, "Active")
+ ]
+ self.assertEqual(schedules, expected_depreciation_before_repair)
+ asset.reload()
+ self.assertEqual(asset.finance_books[0].value_after_depreciation, 500)
+
+ def test_depreciation_schedule_after_cancelling_asset_repair_for_existing_asset(self):
+ asset = create_asset(
+ item_code="Macbook Pro",
+ gross_purchase_amount=500,
+ calculate_depreciation=1,
+ depreciation_method="Straight Line",
+ available_for_use_date="2023-01-15",
+ depreciation_start_date="2023-03-31",
+ frequency_of_depreciation=1,
+ total_number_of_depreciations=12,
+ is_existing_asset=1,
+ opening_accumulated_depreciation=64.52,
+ opening_number_of_booked_depreciations=2,
+ submit=1,
+ )
+
+ expected_depreciation_before_repair = [
+ ["2023-03-31", 41.39, 105.91],
+ ["2023-04-30", 41.39, 147.3],
+ ["2023-05-31", 41.39, 188.69],
+ ["2023-06-30", 41.39, 230.08],
+ ["2023-07-31", 41.39, 271.47],
+ ["2023-08-31", 41.39, 312.86],
+ ["2023-09-30", 41.39, 354.25],
+ ["2023-10-31", 41.39, 395.64],
+ ["2023-11-30", 41.39, 437.03],
+ ["2023-12-31", 41.39, 478.42],
+ ["2024-01-15", 21.58, 500.0],
+ ]
+
+ schedules = [
+ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
+ for d in get_depr_schedule(asset.name, "Active")
+ ]
+ self.assertEqual(schedules, expected_depreciation_before_repair)
+
+ asset_repair = create_asset_repair(
+ asset=asset,
+ capitalize_repair_cost=1,
+ item="_Test Non Stock Item",
+ failure_date="2023-04-01",
+ pi_repair_cost1=60,
+ pi_repair_cost2=40,
+ increase_in_asset_life=0,
+ submit=1,
+ )
+ self.assertEqual(asset_repair.total_repair_cost, 100)
+
+ expected_depreciation_after_repair = [
+ ["2023-03-31", 50.9, 115.42],
+ ["2023-04-30", 50.9, 166.32],
+ ["2023-05-31", 50.9, 217.22],
+ ["2023-06-30", 50.9, 268.12],
+ ["2023-07-31", 50.9, 319.02],
+ ["2023-08-31", 50.9, 369.92],
+ ["2023-09-30", 50.9, 420.82],
+ ["2023-10-31", 50.9, 471.72],
+ ["2023-11-30", 50.9, 522.62],
+ ["2023-12-31", 50.9, 573.52],
+ ["2024-01-15", 26.48, 600.0],
+ ]
+
+ schedules = [
+ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
+ for d in get_depr_schedule(asset.name, "Active")
+ ]
+ self.assertEqual(schedules, expected_depreciation_after_repair)
+
+ asset_repair.cancel()
+
+ schedules = [
+ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
+ for d in get_depr_schedule(asset.name, "Active")
+ ]
+
+ self.assertEqual(schedules, expected_depreciation_before_repair)
+ asset.reload()
+ self.assertEqual(asset.finance_books[0].value_after_depreciation, 435.48)
+
+ def test_wdv_depreciation_schedule_after_cancelling_asset_repair(self):
+ asset = create_asset(
+ item_code="Macbook Pro",
+ gross_purchase_amount=500,
+ calculate_depreciation=1,
+ depreciation_method="Written Down Value",
+ available_for_use_date="2023-04-01",
+ depreciation_start_date="2023-12-31",
+ frequency_of_depreciation=12,
+ total_number_of_depreciations=4,
+ rate_of_depreciation=40,
+ submit=1,
+ )
+
+ expected_depreciation_before_repair = [
+ ["2023-12-31", 150.68, 150.68],
+ ["2024-12-31", 139.73, 290.41],
+ ["2025-12-31", 83.84, 374.25],
+ ["2026-12-31", 50.3, 424.55],
+ ["2027-04-01", 75.45, 500.0],
+ ]
+
+ schedules = [
+ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
+ for d in get_depr_schedule(asset.name, "Active")
+ ]
+ self.assertEqual(schedules, expected_depreciation_before_repair)
+
+ asset_repair = create_asset_repair(
+ asset=asset,
+ capitalize_repair_cost=1,
+ item="_Test Non Stock Item",
+ failure_date="2024-01-01",
+ pi_repair_cost1=60,
+ pi_repair_cost2=40,
+ increase_in_asset_life=0,
+ submit=1,
+ )
+
+ expected_depreciation_after_repair = [
+ ["2023-12-31", 180.82, 180.82],
+ ["2024-12-31", 167.67, 348.49],
+ ["2025-12-31", 100.6, 449.09],
+ ["2026-12-31", 60.36, 509.45],
+ ["2027-04-01", 90.55, 600.0],
+ ]
+
+ schedules = [
+ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
+ for d in get_depr_schedule(asset.name, "Active")
+ ]
+ self.assertEqual(schedules, expected_depreciation_after_repair)
+
+ asset_repair.cancel()
+ schedules = [
+ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
+ for d in get_depr_schedule(asset.name, "Active")
+ ]
+
+ self.assertEqual(schedules, expected_depreciation_before_repair)
+
+ def test_daily_prorata_based_depreciation_schedule_after_cancelling_asset_repair_for(self):
+ asset = create_asset(
+ item_code="Macbook Pro",
+ gross_purchase_amount=500,
+ calculate_depreciation=1,
+ depreciation_method="Straight Line",
+ available_for_use_date="2023-01-01",
+ depreciation_start_date="2023-01-31",
+ daily_prorata_based=1,
+ frequency_of_depreciation=1,
+ total_number_of_depreciations=12,
+ submit=1,
+ )
+
+ expected_depreciation_before_repair = [
+ ["2023-01-31", 42.47, 42.47],
+ ["2023-02-28", 38.36, 80.83],
+ ["2023-03-31", 42.47, 123.3],
+ ["2023-04-30", 41.1, 164.4],
+ ["2023-05-31", 42.47, 206.87],
+ ["2023-06-30", 41.1, 247.97],
+ ["2023-07-31", 42.47, 290.44],
+ ["2023-08-31", 42.47, 332.91],
+ ["2023-09-30", 41.1, 374.01],
+ ["2023-10-31", 42.47, 416.48],
+ ["2023-11-30", 41.1, 457.58],
+ ["2023-12-31", 42.42, 500.0],
+ ]
+
+ schedules = [
+ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
+ for d in get_depr_schedule(asset.name, "Active")
+ ]
+ self.assertEqual(schedules, expected_depreciation_before_repair)
+
+ asset_repair = create_asset_repair(
+ asset=asset,
+ capitalize_repair_cost=1,
+ item="_Test Non Stock Item",
+ failure_date="2023-04-01",
+ pi_repair_cost1=60,
+ pi_repair_cost2=40,
+ increase_in_asset_life=0,
+ submit=1,
+ )
+ self.assertEqual(asset_repair.total_repair_cost, 100)
+
+ expected_depreciation_after_repair = [
+ ["2023-01-31", 50.96, 50.96],
+ ["2023-02-28", 46.03, 96.99],
+ ["2023-03-31", 50.96, 147.95],
+ ["2023-04-30", 49.32, 197.27],
+ ["2023-05-31", 50.96, 248.23],
+ ["2023-06-30", 49.32, 297.55],
+ ["2023-07-31", 50.96, 348.51],
+ ["2023-08-31", 50.96, 399.47],
+ ["2023-09-30", 49.32, 448.79],
+ ["2023-10-31", 50.96, 499.75],
+ ["2023-11-30", 49.32, 549.07],
+ ["2023-12-31", 50.93, 600.0],
+ ]
+
+ schedules = [
+ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
+ for d in get_depr_schedule(asset.name, "Active")
+ ]
+ self.assertEqual(schedules, expected_depreciation_after_repair)
+ asset.reload()
+ self.assertEqual(asset.finance_books[0].value_after_depreciation, 600)
+
+ asset_repair.cancel()
+
+ schedules = [
+ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
+ for d in get_depr_schedule(asset.name, "Active")
+ ]
+ self.assertEqual(schedules, expected_depreciation_before_repair)
+ asset.reload()
+ self.assertEqual(asset.finance_books[0].value_after_depreciation, 500)
+
+ def test_depreciation_schedule_after_cancelling_asset_value_adjustent(self):
+ asset = create_asset(
+ item_code="Macbook Pro",
+ gross_purchase_amount=1000,
+ calculate_depreciation=1,
+ depreciation_method="Straight Line",
+ available_for_use_date="2023-01-01",
+ depreciation_start_date="2023-01-31",
+ frequency_of_depreciation=1,
+ total_number_of_depreciations=12,
+ submit=1,
+ )
+
+ expected_depreciation_before_adjustment = [
+ ["2023-01-31", 83.33, 83.33],
+ ["2023-02-28", 83.33, 166.66],
+ ["2023-03-31", 83.33, 249.99],
+ ["2023-04-30", 83.33, 333.32],
+ ["2023-05-31", 83.33, 416.65],
+ ["2023-06-30", 83.33, 499.98],
+ ["2023-07-31", 83.33, 583.31],
+ ["2023-08-31", 83.33, 666.64],
+ ["2023-09-30", 83.33, 749.97],
+ ["2023-10-31", 83.33, 833.3],
+ ["2023-11-30", 83.33, 916.63],
+ ["2023-12-31", 83.37, 1000.0],
]
schedules = [
- [cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount]
- for d in get_depr_schedule(asset.name, "Draft")
+ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
+ for d in get_depr_schedule(asset.name, "Active")
]
- self.assertEqual(schedules, expected_schedules)
+ self.assertEqual(schedules, expected_depreciation_before_adjustment)
+
+ current_asset_value = asset.finance_books[0].value_after_depreciation
+ asset_value_adjustment = make_asset_value_adjustment(
+ asset=asset,
+ date="2023-04-01",
+ current_asset_value=current_asset_value,
+ new_asset_value=1200,
+ )
+ asset_value_adjustment.submit()
+
+ expected_depreciation_after_adjustment = [
+ ["2023-01-31", 100.0, 100.0],
+ ["2023-02-28", 100.0, 200.0],
+ ["2023-03-31", 100.0, 300.0],
+ ["2023-04-30", 100.0, 400.0],
+ ["2023-05-31", 100.0, 500.0],
+ ["2023-06-30", 100.0, 600.0],
+ ["2023-07-31", 100.0, 700.0],
+ ["2023-08-31", 100.0, 800.0],
+ ["2023-09-30", 100.0, 900.0],
+ ["2023-10-31", 100.0, 1000.0],
+ ["2023-11-30", 100.0, 1100.0],
+ ["2023-12-31", 100.0, 1200.0],
+ ]
+
+ schedules = [
+ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
+ for d in get_depr_schedule(asset.name, "Active")
+ ]
+ self.assertEqual(schedules, expected_depreciation_after_adjustment)
+
+ asset_value_adjustment.cancel()
+ asset.reload()
+ self.assertEqual(asset.finance_books[0].value_after_depreciation, 1000)
+
+ schedules = [
+ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
+ for d in get_depr_schedule(asset.name, "Active")
+ ]
+ self.assertEqual(schedules, expected_depreciation_before_adjustment)
+
+ def test_depreciation_on_return_of_sold_asset(self):
+ from erpnext.controllers.sales_and_purchase_return import make_return_doc
+
+ create_asset_data()
+ asset = create_asset(item_code="Macbook Pro", calculate_depreciation=1, submit=1)
+ post_depreciation_entries(getdate("2021-09-30"))
+
+ si = create_sales_invoice(
+ item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000, posting_date=getdate("2021-09-30")
+ )
+ return_si = make_return_doc("Sales Invoice", si.name)
+ return_si.submit()
+ asset.load_from_db()
+
+ expected_values = [
+ ["2020-06-30", 1366.12, 1366.12, True],
+ ["2021-06-30", 20000.0, 21366.12, True],
+ ["2022-06-30", 20000.95, 41367.07, False],
+ ["2023-06-30", 20000.95, 61368.02, False],
+ ["2024-06-30", 20000.95, 81368.97, False],
+ ["2025-06-06", 18631.03, 100000.0, False],
+ ]
+
+ for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")):
+ self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
+ self.assertEqual(expected_values[i][1], schedule.depreciation_amount)
+ self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
+ self.assertEqual(schedule.journal_entry, schedule.journal_entry)
+
+ def test_depreciation_schedule_after_cancelling_asset_value_adjustent_for_existing_asset(self):
+ asset = create_asset(
+ item_code="Macbook Pro",
+ gross_purchase_amount=500,
+ calculate_depreciation=1,
+ depreciation_method="Straight Line",
+ available_for_use_date="2023-01-15",
+ depreciation_start_date="2023-03-31",
+ frequency_of_depreciation=1,
+ total_number_of_depreciations=12,
+ is_existing_asset=1,
+ opening_accumulated_depreciation=64.52,
+ opening_number_of_booked_depreciations=2,
+ submit=1,
+ )
+
+ expected_depreciation_before_adjustment = [
+ ["2023-03-31", 41.39, 105.91],
+ ["2023-04-30", 41.39, 147.3],
+ ["2023-05-31", 41.39, 188.69],
+ ["2023-06-30", 41.39, 230.08],
+ ["2023-07-31", 41.39, 271.47],
+ ["2023-08-31", 41.39, 312.86],
+ ["2023-09-30", 41.39, 354.25],
+ ["2023-10-31", 41.39, 395.64],
+ ["2023-11-30", 41.39, 437.03],
+ ["2023-12-31", 41.39, 478.42],
+ ["2024-01-15", 21.58, 500.0],
+ ]
+
+ schedules = [
+ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
+ for d in get_depr_schedule(asset.name, "Active")
+ ]
+ self.assertEqual(schedules, expected_depreciation_before_adjustment)
+
+ current_asset_value = asset.finance_books[0].value_after_depreciation
+ asset_value_adjustment = make_asset_value_adjustment(
+ asset=asset,
+ date="2023-04-01",
+ current_asset_value=current_asset_value,
+ new_asset_value=600,
+ )
+ asset_value_adjustment.submit()
+
+ expected_depreciation_after_adjustment = [
+ ["2023-03-31", 57.03, 121.55],
+ ["2023-04-30", 57.03, 178.58],
+ ["2023-05-31", 57.03, 235.61],
+ ["2023-06-30", 57.03, 292.64],
+ ["2023-07-31", 57.03, 349.67],
+ ["2023-08-31", 57.03, 406.7],
+ ["2023-09-30", 57.03, 463.73],
+ ["2023-10-31", 57.03, 520.76],
+ ["2023-11-30", 57.03, 577.79],
+ ["2023-12-31", 57.03, 634.82],
+ ["2024-01-15", 29.7, 664.52],
+ ]
+
+ schedules = [
+ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
+ for d in get_depr_schedule(asset.name, "Active")
+ ]
+ self.assertEqual(schedules, expected_depreciation_after_adjustment)
+
+ asset_value_adjustment.cancel()
+
+ schedules = [
+ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
+ for d in get_depr_schedule(asset.name, "Active")
+ ]
+
+ self.assertEqual(schedules, expected_depreciation_before_adjustment)
+
+ def test_depreciation_schedule_for_parallel_adjustment_and_repair(self):
+ asset = create_asset(
+ item_code="Macbook Pro",
+ gross_purchase_amount=600,
+ calculate_depreciation=1,
+ depreciation_method="Straight Line",
+ available_for_use_date="2021-01-01",
+ depreciation_start_date="2021-12-31",
+ frequency_of_depreciation=12,
+ total_number_of_depreciations=3,
+ is_existing_asset=1,
+ submit=1,
+ )
+ post_depreciation_entries(date="2021-12-31")
+ asset.reload()
+
+ expected_depreciation_before_adjustment = [
+ ["2021-12-31", 200, 200],
+ ["2022-12-31", 200, 400],
+ ["2023-12-31", 200, 600],
+ ]
+
+ schedules = [
+ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
+ for d in get_depr_schedule(asset.name, "Active")
+ ]
+ self.assertEqual(schedules, expected_depreciation_before_adjustment)
+
+ current_asset_value = asset.finance_books[0].value_after_depreciation
+ asset_value_adjustment = make_asset_value_adjustment(
+ asset=asset,
+ date="2022-01-15",
+ current_asset_value=current_asset_value,
+ new_asset_value=500,
+ )
+ asset_value_adjustment.submit()
+
+ expected_depreciation_after_adjustment = [
+ ["2021-12-31", 200, 200],
+ ["2022-12-31", 250, 450],
+ ["2023-12-31", 250, 700],
+ ]
+
+ schedules = [
+ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
+ for d in get_depr_schedule(asset.name, "Active")
+ ]
+ self.assertEqual(schedules, expected_depreciation_after_adjustment)
+
+ asset_repair = create_asset_repair(
+ asset=asset,
+ capitalize_repair_cost=1,
+ item="_Test Non Stock Item",
+ failure_date="2022-01-20",
+ pi_repair_cost1=60,
+ pi_repair_cost2=40,
+ increase_in_asset_life=0,
+ submit=1,
+ )
+ self.assertEqual(asset_repair.total_repair_cost, 100)
+
+ expected_depreciation_after_repair = [
+ ["2021-12-31", 200, 200],
+ ["2022-12-31", 300, 500],
+ ["2023-12-31", 300, 800],
+ ]
+
+ schedules = [
+ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
+ for d in get_depr_schedule(asset.name, "Active")
+ ]
+ self.assertEqual(schedules, expected_depreciation_after_repair)
+ asset.reload()
+
+ asset_value_adjustment.cancel()
+
+ expected_depreciation_after_cancelling_adjustment = [
+ ["2021-12-31", 200, 200],
+ ["2022-12-31", 250, 450],
+ ["2023-12-31", 250, 700],
+ ]
+
+ schedules = [
+ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
+ for d in get_depr_schedule(asset.name, "Active")
+ ]
+
+ self.assertEqual(schedules, expected_depreciation_after_cancelling_adjustment)
+
+ def test_depreciation_schedule_after_sale_of_asset(self):
+ asset = create_asset(
+ item_code="Macbook Pro",
+ gross_purchase_amount=600,
+ calculate_depreciation=1,
+ depreciation_method="Straight Line",
+ available_for_use_date="2021-01-01",
+ depreciation_start_date="2021-12-31",
+ frequency_of_depreciation=12,
+ total_number_of_depreciations=3,
+ is_existing_asset=1,
+ submit=1,
+ )
+ post_depreciation_entries(date="2021-12-31")
+ asset.reload()
+
+ expected_depreciation_before_adjustment = [
+ ["2021-12-31", 200, 200],
+ ["2022-12-31", 200, 400],
+ ["2023-12-31", 200, 600],
+ ]
+
+ schedules = [
+ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
+ for d in get_depr_schedule(asset.name, "Active")
+ ]
+ self.assertEqual(schedules, expected_depreciation_before_adjustment)
+
+ current_asset_value = asset.finance_books[0].value_after_depreciation
+ asset_value_adjustment = make_asset_value_adjustment(
+ asset=asset,
+ date="2022-01-15",
+ current_asset_value=current_asset_value,
+ new_asset_value=500,
+ )
+ asset_value_adjustment.submit()
+
+ expected_depreciation_after_adjustment = [
+ ["2021-12-31", 200, 200],
+ ["2022-12-31", 250, 450],
+ ["2023-12-31", 250, 700],
+ ]
+
+ schedules = [
+ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
+ for d in get_depr_schedule(asset.name, "Active")
+ ]
+ self.assertEqual(schedules, expected_depreciation_after_adjustment)
+
+ si = create_sales_invoice(
+ item_code="Macbook Pro", asset=asset.name, qty=1, rate=300, posting_date=getdate("2022-04-01")
+ )
+ asset.load_from_db()
+
+ self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
+
+ expected_depreciation_after_sale = [
+ ["2021-12-31", 200.0, 200.0],
+ ["2022-04-01", 62.33, 262.33],
+ ]
+
+ schedules = [
+ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
+ for d in get_depr_schedule(asset.name, "Active")
+ ]
+ self.assertEqual(schedules, expected_depreciation_after_sale)
+
+ si.cancel()
+ asset.reload()
+
+ self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated")
+
+ schedules = [
+ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
+ for d in get_depr_schedule(asset.name, "Active")
+ ]
+ self.assertEqual(schedules, expected_depreciation_after_adjustment)
+
+ def test_depreciation_schedule_after_sale_of_asset_wdv_method(self):
+ asset = create_asset(
+ item_code="Macbook Pro",
+ gross_purchase_amount=500,
+ calculate_depreciation=1,
+ depreciation_method="Written Down Value",
+ available_for_use_date="2021-01-01",
+ depreciation_start_date="2021-12-31",
+ rate_of_depreciation=50,
+ frequency_of_depreciation=12,
+ total_number_of_depreciations=3,
+ is_existing_asset=1,
+ submit=1,
+ )
+ post_depreciation_entries(date="2021-12-31")
+ asset.reload()
+
+ expected_depreciation_before_repair = [
+ ["2021-12-31", 250.0, 250.0],
+ ["2022-12-31", 125.0, 375.0],
+ ["2023-12-31", 125.0, 500.0],
+ ]
+
+ schedules = [
+ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
+ for d in get_depr_schedule(asset.name, "Active")
+ ]
+ self.assertEqual(schedules, expected_depreciation_before_repair)
+
+ create_asset_repair(
+ asset=asset,
+ capitalize_repair_cost=1,
+ item="_Test Non Stock Item",
+ failure_date="2022-03-01",
+ pi_repair_cost1=60,
+ pi_repair_cost2=40,
+ increase_in_asset_life=0,
+ submit=1,
+ )
+
+ expected_depreciation_after_repair = [
+ ["2021-12-31", 250.0, 250.0],
+ ["2022-12-31", 175.0, 425.0],
+ ["2023-12-31", 175.0, 600.0],
+ ]
+
+ schedules = [
+ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
+ for d in get_depr_schedule(asset.name, "Active")
+ ]
+ self.assertEqual(schedules, expected_depreciation_after_repair)
+
+ si = create_sales_invoice(
+ item_code="Macbook Pro", asset=asset.name, qty=1, rate=300, posting_date=getdate("2022-04-01")
+ )
+ asset.load_from_db()
+
+ self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
+
+ expected_depreciation_after_sale = [
+ ["2021-12-31", 250.0, 250.0],
+ ["2022-04-01", 43.63, 293.63],
+ ]
+
+ schedules = [
+ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
+ for d in get_depr_schedule(asset.name, "Active")
+ ]
+ self.assertEqual(schedules, expected_depreciation_after_sale)
+
+ si.cancel()
+ asset.reload()
+ self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated")
+
+ schedules = [
+ [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
+ for d in get_depr_schedule(asset.name, "Active")
+ ]
+ self.assertEqual(schedules, expected_depreciation_after_repair)
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 60e8778e0e6..eb726a1a1f7 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",
"frequency_of_depreciation",
"total_number_of_depreciations",
+ "increase_in_asset_life",
"depreciation_start_date",
"column_break_5",
"salvage_value_percentage",
@@ -89,7 +90,8 @@
"fieldname": "rate_of_depreciation",
"fieldtype": "Percent",
"label": "Rate of Depreciation (%)",
- "mandatory_depends_on": "eval:doc.depreciation_method == 'Written Down Value'"
+ "mandatory_depends_on": "eval:doc.depreciation_method == 'Written Down Value'",
+ "no_copy": 1
},
{
"fieldname": "salvage_value_percentage",
@@ -115,6 +117,7 @@
"fieldname": "total_number_of_booked_depreciations",
"fieldtype": "Int",
"label": "Total Number of Booked Depreciations ",
+ "no_copy": 1,
"read_only": 1
},
{
@@ -124,12 +127,20 @@
{
"fieldname": "column_break_sigk",
"fieldtype": "Column Break"
+ },
+ {
+ "description": "via Asset Repair",
+ "fieldname": "increase_in_asset_life",
+ "fieldtype": "Int",
+ "label": "Increase In Asset Life (Months)",
+ "no_copy": 1,
+ "read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2024-12-13 12:11:03.743209",
+ "modified": "2025-01-06 17:14:51.836803",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Finance Book",
diff --git a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.py b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.py
index d06d6355ec3..d629aefd967 100644
--- a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.py
+++ b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.py
@@ -22,6 +22,7 @@ class AssetFinanceBook(Document):
expected_value_after_useful_life: DF.Currency
finance_book: DF.Link | None
frequency_of_depreciation: DF.Int
+ increase_in_asset_life: DF.Int
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js
index 167bef414f3..3ce1d5390db 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.js
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.js
@@ -34,7 +34,16 @@ frappe.ui.form.on("Asset Repair", {
query: "erpnext.assets.doctype.asset_repair.asset_repair.get_purchase_invoice",
filters: {
company: frm.doc.company,
- docstatus: 1,
+ },
+ };
+ });
+
+ frm.set_query("expense_account", "invoices", function (doc, cdt, cdn) {
+ let row = locals[cdt][cdn];
+ return {
+ query: "erpnext.assets.doctype.asset_repair.asset_repair.get_expense_accounts",
+ filters: {
+ purchase_invoice: row.purchase_invoice,
},
};
});
@@ -59,16 +68,6 @@ frappe.ui.form.on("Asset Repair", {
},
};
});
-
- frm.set_query("expense_account", "invoices", function () {
- return {
- filters: {
- company: frm.doc.company,
- is_group: ["=", 0],
- report_type: ["=", "Profit and Loss"],
- },
- };
- });
},
refresh: function (frm) {
diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json
index 8195c56b431..f49de50838a 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.json
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.json
@@ -7,40 +7,42 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
+ "naming_series",
"asset",
+ "asset_name",
"company",
"column_break_2",
- "asset_name",
- "naming_series",
- "section_break_5",
- "failure_date",
"repair_status",
- "column_break_6",
+ "failure_date",
"completion_date",
- "accounting_dimensions_section",
- "cost_center",
- "column_break_14",
- "project",
- "accounting_details",
- "invoices",
- "section_break_y7cc",
- "capitalize_repair_cost",
- "stock_consumption",
- "column_break_8",
- "repair_cost",
- "stock_consumption_details_section",
- "stock_items",
- "total_repair_cost",
- "asset_depreciation_details_section",
- "increase_in_asset_life",
+ "downtime",
+ "amended_from",
"section_break_9",
"description",
"column_break_9",
"actions_performed",
- "section_break_23",
- "downtime",
- "column_break_19",
- "amended_from"
+ "accounting_details",
+ "invoices",
+ "section_break_muyc",
+ "column_break_ajbh",
+ "column_break_hkem",
+ "repair_cost",
+ "accounting_dimensions_section",
+ "cost_center",
+ "column_break_14",
+ "project",
+ "stock_consumption_details_section",
+ "stock_items",
+ "section_break_ltbb",
+ "column_break_ewor",
+ "column_break_ceuc",
+ "consumed_items_cost",
+ "capitalizations_section",
+ "column_break_spre",
+ "capitalize_repair_cost",
+ "increase_in_asset_life",
+ "column_break_xebe",
+ "total_repair_cost"
],
"fields": [
{
@@ -54,11 +56,6 @@
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
- {
- "fieldname": "section_break_5",
- "fieldtype": "Section Break",
- "label": "Repair Details"
- },
{
"columns": 1,
"fieldname": "failure_date",
@@ -67,11 +64,7 @@
"reqd": 1
},
{
- "fieldname": "column_break_6",
- "fieldtype": "Column Break"
- },
- {
- "depends_on": "eval:!doc.__islocal",
+ "depends_on": "eval:doc.repair_status==\"Completed\"",
"fieldname": "completion_date",
"fieldtype": "Datetime",
"label": "Completion Date",
@@ -79,7 +72,6 @@
},
{
"default": "Pending",
- "depends_on": "eval:!doc.__islocal",
"fieldname": "repair_status",
"fieldtype": "Select",
"label": "Repair Status",
@@ -113,10 +105,6 @@
"label": "Downtime",
"read_only": 1
},
- {
- "fieldname": "column_break_19",
- "fieldtype": "Column Break"
- },
{
"default": "0",
"fieldname": "repair_cost",
@@ -148,10 +136,6 @@
"fieldtype": "Read Only",
"label": "Asset Name"
},
- {
- "fieldname": "column_break_8",
- "fieldtype": "Column Break"
- },
{
"default": "0",
"depends_on": "eval:!doc.__islocal",
@@ -162,7 +146,8 @@
{
"fieldname": "accounting_details",
"fieldtype": "Section Break",
- "label": "Accounting Details"
+ "hide_border": 1,
+ "label": "Repair Purchase Invoices"
},
{
"fieldname": "stock_items",
@@ -172,16 +157,7 @@
"options": "Asset Repair Consumed Item"
},
{
- "fieldname": "section_break_23",
- "fieldtype": "Section Break"
- },
- {
- "collapsible": 1,
- "fieldname": "accounting_dimensions_section",
- "fieldtype": "Section Break",
- "label": "Accounting Dimensions"
- },
- {
+ "fetch_from": "company.cost_center",
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
@@ -198,21 +174,13 @@
"fieldtype": "Column Break"
},
{
- "default": "0",
- "depends_on": "eval:!doc.__islocal",
- "fieldname": "stock_consumption",
- "fieldtype": "Check",
- "label": "Stock Consumed During Repair"
- },
- {
- "depends_on": "stock_consumption",
+ "collapsible_depends_on": "stock_items",
"fieldname": "stock_consumption_details_section",
"fieldtype": "Section Break",
- "label": "Stock Consumption Details"
+ "hide_border": 1,
+ "label": "Consumed Stock Items"
},
{
- "depends_on": "eval: doc.stock_consumption && doc.total_repair_cost > 0",
- "description": "Sum of Repair Cost and Value of Consumed Stock Items.",
"fieldname": "total_repair_cost",
"fieldtype": "Currency",
"label": "Total Repair Cost",
@@ -220,11 +188,6 @@
},
{
"depends_on": "capitalize_repair_cost",
- "fieldname": "asset_depreciation_details_section",
- "fieldtype": "Section Break",
- "label": "Asset Depreciation Details"
- },
- {
"fieldname": "increase_in_asset_life",
"fieldtype": "Int",
"label": "Increase In Asset Life(Months)",
@@ -240,20 +203,63 @@
{
"fieldname": "invoices",
"fieldtype": "Table",
- "label": "Asset Repair Purchase Invoices",
"mandatory_depends_on": "eval: doc.repair_status == 'Completed' && doc.repair_cost > 0;",
"no_copy": 1,
"options": "Asset Repair Purchase Invoice"
},
{
- "fieldname": "section_break_y7cc",
+ "fieldname": "section_break_muyc",
"fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_hkem",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "capitalizations_section",
+ "fieldtype": "Section Break",
+ "label": "Totals"
+ },
+ {
+ "fieldname": "column_break_spre",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "column_break_ajbh",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_ltbb",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_ewor",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "column_break_ceuc",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "consumed_items_cost",
+ "fieldtype": "Currency",
+ "label": "Consumed Items Cost"
+ },
+ {
+ "fieldname": "column_break_xebe",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "capitalize_repair_cost",
+ "fieldname": "accounting_dimensions_section",
+ "fieldtype": "Section Break",
+ "label": "Accounting Dimensions"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2024-09-30 13:02:06.931188",
+ "modified": "2024-12-27 18:11:40.548727",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Repair",
diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py
index c6ae3c5d8e4..8a4349233e5 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.py
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.py
@@ -4,7 +4,7 @@
import frappe
from frappe import _
from frappe.query_builder import DocType
-from frappe.utils import add_months, cint, flt, get_link_to_form, getdate, time_diff_in_hours
+from frappe.utils import cint, flt, get_link_to_form, getdate, time_diff_in_hours
import erpnext
from erpnext.accounts.general_ledger import make_gl_entries
@@ -12,7 +12,7 @@ from erpnext.assets.doctype.asset.asset import get_asset_account
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_depr_schedule,
- make_new_active_asset_depr_schedules_and_cancel_current_ones,
+ reschedule_depreciation,
)
from erpnext.controllers.accounts_controller import AccountsController
@@ -40,6 +40,7 @@ class AssetRepair(AccountsController):
capitalize_repair_cost: DF.Check
company: DF.Link | None
completion_date: DF.Datetime | None
+ consumed_items_cost: DF.Currency
cost_center: DF.Link | None
description: DF.LongText | None
downtime: DF.Data | None
@@ -50,7 +51,6 @@ class AssetRepair(AccountsController):
project: DF.Link | None
repair_cost: DF.Currency
repair_status: DF.Literal["Pending", "Completed", "Cancelled"]
- stock_consumption: DF.Check
stock_items: DF.Table[AssetRepairConsumedItem]
total_repair_cost: DF.Currency
# end: auto-generated types
@@ -58,16 +58,12 @@ class AssetRepair(AccountsController):
def validate(self):
self.asset_doc = frappe.get_doc("Asset", self.asset)
self.validate_dates()
- self.validate_purchase_invoice()
- self.validate_purchase_invoice_repair_cost()
- self.validate_purchase_invoice_expense_account()
+ self.validate_purchase_invoices()
self.update_status()
-
- if self.get("stock_items"):
- self.set_stock_items_cost()
-
+ self.calculate_consumed_items_cost()
self.calculate_repair_cost()
self.calculate_total_repair_cost()
+ self.check_repair_status()
def validate_dates(self):
if self.completion_date and (self.failure_date > self.completion_date):
@@ -75,36 +71,58 @@ class AssetRepair(AccountsController):
_("Completion Date can not be before Failure Date. Please adjust the dates accordingly.")
)
- def validate_purchase_invoice(self):
- query = expense_item_pi_query(self.company)
- purchase_invoice_list = [item[0] for item in query.run()]
- for pi in self.invoices:
- if pi.purchase_invoice not in purchase_invoice_list:
- frappe.throw(_("Expense item not present in Purchase Invoice"))
+ def validate_purchase_invoices(self):
+ for d in self.invoices:
+ invoice_items = self.get_invoice_items(d.purchase_invoice)
+ self.validate_service_purchase_invoice(d.purchase_invoice, invoice_items)
+ self.validate_expense_account(d, invoice_items)
+ self.validate_purchase_invoice_repair_cost(d, invoice_items)
- def validate_purchase_invoice_repair_cost(self):
- for pi in self.invoices:
- if flt(pi.repair_cost) > frappe.db.get_value(
- "Purchase Invoice", pi.purchase_invoice, "base_net_total"
- ):
- frappe.throw(_("Repair cost cannot be greater than purchase invoice base net total"))
+ def get_invoice_items(self, pi):
+ invoice_items = frappe.get_all(
+ "Purchase Invoice Item",
+ filters={"parent": pi},
+ fields=["item_code", "expense_account", "base_net_amount"],
+ )
- def validate_purchase_invoice_expense_account(self):
- for pi in self.invoices:
- if pi.expense_account not in frappe.db.get_all(
- "Purchase Invoice Item", {"parent": pi.purchase_invoice}, pluck="expense_account"
- ):
- frappe.throw(
- _("Expense account not present in Purchase Invoice {0}").format(
- get_link_to_form("Purchase Invoice", pi.purchase_invoice)
- )
+ return invoice_items
+
+ def validate_service_purchase_invoice(self, purchase_invoice, invoice_items):
+ service_item_exists = False
+ for item in invoice_items:
+ if frappe.db.get_value("Item", item.item_code, "is_stock_item") == 0:
+ service_item_exists = True
+ break
+
+ if not service_item_exists:
+ frappe.throw(
+ _("Service item not present in Purchase Invoice {0}").format(
+ get_link_to_form("Purchase Invoice", purchase_invoice)
)
+ )
+
+ def validate_expense_account(self, row, invoice_items):
+ pi_expense_accounts = set([item.expense_account for item in invoice_items])
+ if row.expense_account not in pi_expense_accounts:
+ frappe.throw(
+ _("Expense account {0} not present in Purchase Invoice {1}").format(
+ row.expense_account, get_link_to_form("Purchase Invoice", row.purchase_invoice)
+ )
+ )
+
+ def validate_purchase_invoice_repair_cost(self, row, invoice_items):
+ pi_net_total = sum([flt(item.base_net_amount) for item in invoice_items])
+ if flt(row.repair_cost) > pi_net_total:
+ frappe.throw(
+ _("Repair cost cannot be greater than purchase invoice base net total {0}").format(
+ pi_net_total
+ )
+ )
def update_status(self):
if self.repair_status == "Pending" and self.asset_doc.status != "Out of Order":
frappe.db.set_value("Asset", self.asset, "status", "Out of Order")
- add_asset_activity(
- self.asset,
+ self.add_asset_activity(
_("Asset out of order due to Asset Repair {0}").format(
get_link_to_form("Asset Repair", self.name)
),
@@ -112,147 +130,78 @@ class AssetRepair(AccountsController):
else:
self.asset_doc.set_status()
- def set_stock_items_cost(self):
+ def calculate_consumed_items_cost(self):
+ consumed_items_cost = 0.0
for item in self.get("stock_items"):
item.total_value = flt(item.valuation_rate) * flt(item.consumed_quantity)
+ consumed_items_cost += item.total_value
+ self.consumed_items_cost = consumed_items_cost
def calculate_repair_cost(self):
self.repair_cost = sum(flt(pi.repair_cost) for pi in self.invoices)
def calculate_total_repair_cost(self):
- self.total_repair_cost = flt(self.repair_cost)
+ self.total_repair_cost = flt(self.repair_cost) + flt(self.consumed_items_cost)
- total_value_of_stock_consumed = self.get_total_value_of_stock_consumed()
- self.total_repair_cost += total_value_of_stock_consumed
+ def on_submit(self):
+ self.decrease_stock_quantity()
- def before_submit(self):
- self.check_repair_status()
+ if self.get("capitalize_repair_cost"):
+ self.update_asset_value()
+ self.set_increase_in_asset_life()
- self.asset_doc.flags.increase_in_asset_value_due_to_repair = False
+ depreciation_note = self.get_depreciation_note()
+ reschedule_depreciation(self.asset_doc, depreciation_note)
+ self.add_asset_activity()
- if self.get("stock_consumption") or self.get("capitalize_repair_cost"):
- self.asset_doc.flags.increase_in_asset_value_due_to_repair = True
+ self.make_gl_entries()
- self.increase_asset_value()
-
- total_repair_cost = self.get_total_value_of_stock_consumed()
- if self.capitalize_repair_cost:
- total_repair_cost += self.repair_cost
- self.asset_doc.total_asset_cost += total_repair_cost
- self.asset_doc.additional_asset_cost += total_repair_cost
-
- if self.get("stock_consumption"):
- self.check_for_stock_items_and_warehouse()
- self.decrease_stock_quantity()
- if self.get("capitalize_repair_cost"):
- self.make_gl_entries()
- if self.asset_doc.calculate_depreciation and self.increase_in_asset_life:
- self.modify_depreciation_schedule()
-
- notes = _(
- "This schedule was created when Asset {0} was repaired through Asset Repair {1}."
- ).format(
- get_link_to_form(self.asset_doc.doctype, self.asset_doc.name),
- get_link_to_form(self.doctype, self.name),
- )
- self.asset_doc.flags.ignore_validate_update_after_submit = True
- make_new_active_asset_depr_schedules_and_cancel_current_ones(
- self.asset_doc, notes, ignore_booked_entry=True
- )
- self.asset_doc.save()
-
- add_asset_activity(
- self.asset,
- _("Asset updated after completion of Asset Repair {0}").format(
- get_link_to_form("Asset Repair", self.name)
- ),
- )
-
- def before_cancel(self):
+ def on_cancel(self):
self.asset_doc = frappe.get_doc("Asset", self.asset)
+ if self.get("capitalize_repair_cost"):
+ self.update_asset_value()
+ self.make_gl_entries(cancel=True)
+ self.set_increase_in_asset_life()
- self.asset_doc.flags.increase_in_asset_value_due_to_repair = False
-
- if self.get("stock_consumption") or self.get("capitalize_repair_cost"):
- self.asset_doc.flags.increase_in_asset_value_due_to_repair = True
-
- self.decrease_asset_value()
-
- total_repair_cost = self.get_total_value_of_stock_consumed()
- if self.capitalize_repair_cost:
- total_repair_cost += self.repair_cost
- self.asset_doc.total_asset_cost -= total_repair_cost
- self.asset_doc.additional_asset_cost -= total_repair_cost
-
- if self.get("capitalize_repair_cost"):
- self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
- self.make_gl_entries(cancel=True)
- if self.asset_doc.calculate_depreciation and self.increase_in_asset_life:
- self.revert_depreciation_schedule_on_cancellation()
-
- notes = _(
- "This schedule was created when Asset {0}'s Asset Repair {1} was cancelled."
- ).format(
- get_link_to_form(self.asset_doc.doctype, self.asset_doc.name),
- get_link_to_form(self.doctype, self.name),
- )
- self.asset_doc.flags.ignore_validate_update_after_submit = True
- make_new_active_asset_depr_schedules_and_cancel_current_ones(
- self.asset_doc, notes, ignore_booked_entry=True
- )
- self.asset_doc.save()
-
- add_asset_activity(
- self.asset,
- _("Asset updated after cancellation of Asset Repair {0}").format(
- get_link_to_form("Asset Repair", self.name)
- ),
- )
+ depreciation_note = self.get_depreciation_note()
+ reschedule_depreciation(self.asset_doc, depreciation_note)
+ self.add_asset_activity()
def after_delete(self):
frappe.get_doc("Asset", self.asset).set_status()
def check_repair_status(self):
- if self.repair_status == "Pending":
+ if self.repair_status == "Pending" and self.docstatus == 1:
frappe.throw(_("Please update Repair Status."))
- def check_for_stock_items_and_warehouse(self):
- if not self.get("stock_items"):
- frappe.throw(_("Please enter Stock Items consumed during the Repair."), title=_("Missing Items"))
+ def update_asset_value(self):
+ total_repair_cost = self.total_repair_cost if self.docstatus == 1 else -1 * self.total_repair_cost
- def increase_asset_value(self):
- total_value_of_stock_consumed = self.get_total_value_of_stock_consumed()
+ self.asset_doc.total_asset_cost += flt(total_repair_cost)
+ self.asset_doc.additional_asset_cost += flt(total_repair_cost)
if self.asset_doc.calculate_depreciation:
for row in self.asset_doc.finance_books:
- row.value_after_depreciation += total_value_of_stock_consumed
+ row.value_after_depreciation += flt(total_repair_cost)
- if self.capitalize_repair_cost:
- row.value_after_depreciation += self.repair_cost
-
- def decrease_asset_value(self):
- total_value_of_stock_consumed = self.get_total_value_of_stock_consumed()
-
- if self.asset_doc.calculate_depreciation:
- for row in self.asset_doc.finance_books:
- row.value_after_depreciation -= total_value_of_stock_consumed
-
- if self.capitalize_repair_cost:
- row.value_after_depreciation -= self.repair_cost
+ self.asset_doc.flags.ignore_validate_update_after_submit = True
+ self.asset_doc.save()
def get_total_value_of_stock_consumed(self):
- total_value_of_stock_consumed = 0
- if self.get("stock_consumption"):
- for item in self.get("stock_items"):
- total_value_of_stock_consumed += item.total_value
-
- return total_value_of_stock_consumed
+ return sum([flt(item.total_value) for item in self.get("stock_items")])
def decrease_stock_quantity(self):
+ if not self.get("stock_items"):
+ return
+
stock_entry = frappe.get_doc(
- {"doctype": "Stock Entry", "stock_entry_type": "Material Issue", "company": self.company}
+ {
+ "doctype": "Stock Entry",
+ "stock_entry_type": "Material Issue",
+ "company": self.company,
+ "asset_repair": self.name,
+ }
)
- stock_entry.asset_repair = self.name
for stock_item in self.get("stock_items"):
self.validate_serial_no(stock_item)
@@ -278,7 +227,7 @@ class AssetRepair(AccountsController):
"Item", stock_item.item_code, "has_serial_no"
):
msg = f"Serial No Bundle is mandatory for Item {stock_item.item_code}"
- frappe.throw(msg, title=_("Missing Serial No Bundle"))
+ frappe.throw(_(msg), title=_("Missing Serial No Bundle"))
if stock_item.serial_and_batch_bundle:
values_to_update = {
@@ -291,6 +240,9 @@ class AssetRepair(AccountsController):
)
def make_gl_entries(self, cancel=False):
+ if cancel:
+ self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
+
if flt(self.total_repair_cost) > 0:
gl_entries = self.get_gl_entries()
make_gl_entries(gl_entries, cancel)
@@ -348,7 +300,7 @@ class AssetRepair(AccountsController):
)
def get_gl_entries_for_consumed_items(self, gl_entries, fixed_asset_account):
- if not (self.get("stock_consumption") and self.get("stock_items")):
+ if not self.get("stock_items"):
return
# creating GL Entries for each row in Stock Items based on the Stock Entry created for it
@@ -400,72 +352,28 @@ class AssetRepair(AccountsController):
)
)
- def modify_depreciation_schedule(self):
- for row in self.asset_doc.finance_books:
- row.total_number_of_depreciations += self.increase_in_asset_life / row.frequency_of_depreciation
+ def set_increase_in_asset_life(self):
+ if self.asset_doc.calculate_depreciation and cint(self.increase_in_asset_life) > 0:
+ for row in self.asset_doc.finance_books:
+ row.increase_in_asset_life = cint(row.increase_in_asset_life) + (
+ cint(self.increase_in_asset_life) * (1 if self.docstatus == 1 else -1)
+ )
+ row.db_update()
- self.asset_doc.flags.increase_in_asset_life = False
- extra_months = self.increase_in_asset_life % row.frequency_of_depreciation
- if extra_months != 0:
- self.calculate_last_schedule_date(self.asset_doc, row, extra_months)
-
- # to help modify depreciation schedule when increase_in_asset_life is not a multiple of frequency_of_depreciation
- def calculate_last_schedule_date(self, asset, row, extra_months):
- asset.flags.increase_in_asset_life = True
- number_of_pending_depreciations = cint(row.total_number_of_depreciations) - cint(
- asset.opening_number_of_booked_depreciations
+ def get_depreciation_note(self):
+ return _("This schedule was created when Asset {0} was repaired through Asset Repair {1}.").format(
+ get_link_to_form(self.asset_doc.doctype, self.asset_doc.name),
+ get_link_to_form(self.doctype, self.name),
)
- depr_schedule = get_depr_schedule(asset.name, "Active", row.finance_book)
+ def add_asset_activity(self, subject=None):
+ if not subject:
+ subject = _("Asset updated due to Asset Repair {0} {1}.").format(
+ get_link_to_form(self.doctype, self.name),
+ "submission" if self.docstatus == 1 else "cancellation",
+ )
- # the Schedule Date in the final row of the old Depreciation Schedule
- last_schedule_date = depr_schedule[len(depr_schedule) - 1].schedule_date
-
- # the Schedule Date in the final row of the new Depreciation Schedule
- asset.to_date = add_months(last_schedule_date, extra_months)
-
- # the latest possible date at which the depreciation can occur, without increasing the Total Number of Depreciations
- # if depreciations happen yearly and the Depreciation Posting Date is 01-01-2020, this could be 01-01-2021, 01-01-2022...
- schedule_date = add_months(
- row.depreciation_start_date,
- number_of_pending_depreciations * cint(row.frequency_of_depreciation),
- )
-
- if asset.to_date > schedule_date:
- row.total_number_of_depreciations += 1
-
- def revert_depreciation_schedule_on_cancellation(self):
- for row in self.asset_doc.finance_books:
- row.total_number_of_depreciations -= self.increase_in_asset_life / row.frequency_of_depreciation
-
- self.asset_doc.flags.increase_in_asset_life = False
- extra_months = self.increase_in_asset_life % row.frequency_of_depreciation
- if extra_months != 0:
- self.calculate_last_schedule_date_before_modification(self.asset_doc, row, extra_months)
-
- def calculate_last_schedule_date_before_modification(self, asset, row, extra_months):
- asset.flags.increase_in_asset_life = True
- number_of_pending_depreciations = cint(row.total_number_of_depreciations) - cint(
- asset.opening_number_of_booked_depreciations
- )
-
- depr_schedule = get_depr_schedule(asset.name, "Active", row.finance_book)
-
- # the Schedule Date in the final row of the modified Depreciation Schedule
- last_schedule_date = depr_schedule[len(depr_schedule) - 1].schedule_date
-
- # the Schedule Date in the final row of the original Depreciation Schedule
- asset.to_date = add_months(last_schedule_date, -extra_months)
-
- # the latest possible date at which the depreciation can occur, without decreasing the Total Number of Depreciations
- # if depreciations happen yearly and the Depreciation Posting Date is 01-01-2020, this could be 01-01-2021, 01-01-2022...
- schedule_date = add_months(
- row.depreciation_start_date,
- (number_of_pending_depreciations - 1) * cint(row.frequency_of_depreciation),
- )
-
- if asset.to_date < schedule_date:
- row.total_number_of_depreciations -= 1
+ add_asset_activity(self.asset, subject)
@frappe.whitelist()
@@ -476,16 +384,11 @@ def get_downtime(failure_date, completion_date):
@frappe.whitelist()
def get_purchase_invoice(doctype, txt, searchfield, start, page_len, filters):
- query = expense_item_pi_query(filters.get("company"))
- return query.run(as_list=1)
-
-
-def expense_item_pi_query(company):
PurchaseInvoice = DocType("Purchase Invoice")
PurchaseInvoiceItem = DocType("Purchase Invoice Item")
Item = DocType("Item")
- query = (
+ return (
frappe.qb.from_(PurchaseInvoice)
.join(PurchaseInvoiceItem)
.on(PurchaseInvoiceItem.parent == PurchaseInvoice.name)
@@ -495,8 +398,18 @@ def expense_item_pi_query(company):
.where(
(Item.is_stock_item == 0)
& (Item.is_fixed_asset == 0)
- & (PurchaseInvoice.company == company)
+ & (PurchaseInvoice.company == filters.get("company"))
& (PurchaseInvoice.docstatus == 1)
)
- )
- return query
+ ).run(as_list=1)
+
+
+@frappe.whitelist()
+def get_expense_accounts(doctype, txt, searchfield, start, page_len, filters):
+ PurchaseInvoiceItem = DocType("Purchase Invoice Item")
+ return (
+ frappe.qb.from_(PurchaseInvoiceItem)
+ .select(PurchaseInvoiceItem.expense_account)
+ .distinct()
+ .where(PurchaseInvoiceItem.parent == filters.get("purchase_invoice"))
+ ).run(as_list=1)
diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py
index 3ce3bbcb9c4..f0b4f4fb6b2 100644
--- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py
+++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py
@@ -128,7 +128,11 @@ class TestAssetRepair(IntegrationTestCase):
asset = create_asset(calculate_depreciation=1, submit=1)
initial_asset_value = get_asset_value_after_depreciation(asset.name)
asset_repair = create_asset_repair(
- asset=asset, capitalize_repair_cost=1, item="_Test Non Stock Item", submit=1
+ asset=asset,
+ capitalize_repair_cost=1,
+ item="_Test Non Stock Item",
+ submit=1,
+ increase_in_asset_value=1,
)
asset.reload()
@@ -136,7 +140,9 @@ class TestAssetRepair(IntegrationTestCase):
self.assertEqual(asset_repair.repair_cost, increase_in_asset_value)
def test_purchase_invoice(self):
- asset_repair = create_asset_repair(capitalize_repair_cost=1, item="_Test Non Stock Item", submit=1)
+ asset_repair = create_asset_repair(
+ capitalize_repair_cost=1, item="_Test Non Stock Item", submit=1, increase_in_asset_value=1
+ )
self.assertTrue(asset_repair.invoices)
def test_gl_entries_with_perpetual_inventory(self):
@@ -163,6 +169,7 @@ class TestAssetRepair(IntegrationTestCase):
pi_expense_account1="Administrative Expenses - TCP1",
pi_expense_account2="Legal Expenses - TCP1",
item="_Test Non Stock Item",
+ increase_in_asset_life=1,
submit=1,
)
@@ -210,6 +217,7 @@ class TestAssetRepair(IntegrationTestCase):
asset_repair = create_asset_repair(
capitalize_repair_cost=1,
stock_consumption=1,
+ increase_in_asset_life=1,
item="_Test Non Stock Item",
submit=1,
)
@@ -259,7 +267,13 @@ class TestAssetRepair(IntegrationTestCase):
self.assertEqual(first_asset_depr_schedule.status, "Active")
initial_num_of_depreciations = num_of_depreciations(asset)
- create_asset_repair(asset=asset, capitalize_repair_cost=1, item="_Test Non Stock Item", submit=1)
+ create_asset_repair(
+ asset=asset,
+ capitalize_repair_cost=1,
+ item="_Test Non Stock Item",
+ submit=1,
+ increase_in_asset_life=1,
+ )
asset.reload()
first_asset_depr_schedule.load_from_db()
@@ -282,7 +296,9 @@ class TestAssetRepair(IntegrationTestCase):
def num_of_depreciations(asset):
- return asset.finance_books[0].total_number_of_depreciations
+ return asset.finance_books[0].total_number_of_depreciations + (
+ asset.finance_books[0].increase_in_asset_life / 12
+ )
def create_asset_repair(**args):
@@ -300,7 +316,7 @@ def create_asset_repair(**args):
{
"asset": asset.name,
"asset_name": asset.asset_name,
- "failure_date": nowdate(),
+ "failure_date": args.failure_date or nowdate(),
"description": "Test Description",
"company": asset.company,
}
@@ -364,7 +380,7 @@ def create_asset_repair(**args):
if args.capitalize_repair_cost:
asset_repair.capitalize_repair_cost = 1
- if asset.calculate_depreciation:
+ if asset.calculate_depreciation and args.increase_in_asset_life:
asset_repair.increase_in_asset_life = 12
pi1 = make_purchase_invoice(
company=asset.company,
diff --git a/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.js b/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.js
index c22feb05458..98903048f79 100644
--- a/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.js
+++ b/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.js
@@ -2,6 +2,15 @@
// For license information, please see license.txt
frappe.ui.form.on("Asset Shift Allocation", {
onload: function (frm) {
+ frm.set_query("asset", function () {
+ return {
+ filters: {
+ company: frm.doc.company,
+ docstatus: 1,
+ },
+ };
+ });
+
frm.events.make_schedules_editable(frm);
},
diff --git a/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.json b/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.json
index 7693037042d..22c274df970 100644
--- a/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.json
+++ b/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.json
@@ -12,6 +12,7 @@
"finance_book",
"amended_from",
"depreciation_schedule_section",
+ "column_break_jomc",
"depreciation_schedule"
],
"fields": [
@@ -57,7 +58,9 @@
"fieldname": "depreciation_schedule",
"fieldtype": "Table",
"label": "Depreciation Schedule",
- "options": "Depreciation Schedule"
+ "no_copy": 1,
+ "options": "Depreciation Schedule",
+ "read_only": 1
},
{
"fieldname": "naming_series",
@@ -65,12 +68,16 @@
"label": "Naming Series",
"options": "ACC-ASA-.YYYY.-",
"reqd": 1
+ },
+ {
+ "fieldname": "column_break_jomc",
+ "fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2024-03-27 13:06:35.732191",
+ "modified": "2025-01-10 16:25:31.397325",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Shift Allocation",
diff --git a/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.py b/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.py
index 323cb73fb8e..e273b997a3d 100644
--- a/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.py
+++ b/erpnext/assets/doctype/asset_shift_allocation/asset_shift_allocation.py
@@ -17,7 +17,7 @@ from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activ
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,
+ get_temp_depr_schedule_doc,
)
@@ -30,9 +30,7 @@ class AssetShiftAllocation(Document):
if TYPE_CHECKING:
from frappe.types import DF
- from erpnext.assets.doctype.depreciation_schedule.depreciation_schedule import (
- DepreciationSchedule,
- )
+ from erpnext.assets.doctype.depreciation_schedule.depreciation_schedule import DepreciationSchedule
amended_from: DF.Link | None
asset: DF.Link
@@ -41,32 +39,138 @@ class AssetShiftAllocation(Document):
naming_series: DF.Literal["ACC-ASA-.YYYY.-"]
# end: auto-generated types
- 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)
+ if self.get("depreciation_schedule") and self.docstatus == 0:
+ self.validate_invalid_shift_change()
+ self.update_depr_schedule()
- self.validate_invalid_shift_change()
- self.update_depr_schedule()
+ def after_insert(self):
+ self.fetch_and_set_depr_schedule()
def on_submit(self):
self.create_new_asset_depr_schedule()
+ def validate_invalid_shift_change(self):
+ 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):
+ self.adjust_depr_shifts()
+
+ asset_doc = frappe.get_doc("Asset", self.asset)
+ fb_row = self.get_finance_book_row(asset_doc)
+
+ temp_depr_schedule_doc = get_temp_depr_schedule_doc(
+ asset_doc, fb_row, updated_depr_schedule=self.depreciation_schedule
+ )
+
+ # Update the depreciation schedule with the new shifts
+ self.depreciation_schedule = []
+ self.modify_depr_schedule(temp_depr_schedule_doc.get("depreciation_schedule"))
+
+ def adjust_depr_shifts(self):
+ """
+ Adjust the shifts in the depreciation schedule based on the new shifts
+ """
+ shift_factors_map = get_asset_shift_factors_map()
+ reverse_shift_factors_map = {v: k for k, v in shift_factors_map.items()}
+ factor_diff = self.calculate_shift_factor_diff(shift_factors_map)
+
+ # Case 1: Reduce shifts if there is an excess factor
+ if factor_diff > 0:
+ self.reduce_depr_shifts(factor_diff, shift_factors_map, reverse_shift_factors_map)
+
+ # Case 2: Add shifts if there is a missing factor
+ elif factor_diff < 0:
+ self.add_depr_shifts(factor_diff, shift_factors_map, reverse_shift_factors_map)
+
+ def calculate_shift_factor_diff(self, shift_factors_map):
+ original_shift_sum = sum(
+ shift_factors_map.get(schedule.shift, 0)
+ for schedule in self.asset_depr_schedule_doc.depreciation_schedule
+ )
+ new_shift_sum = sum(
+ shift_factors_map.get(schedule.shift, 0) for schedule in self.depreciation_schedule
+ )
+ return new_shift_sum - original_shift_sum
+
+ def reduce_depr_shifts(self, factor_diff, shift_factors_map, reverse_shift_factors_map):
+ for i, schedule in reversed(list(enumerate(self.depreciation_schedule))):
+ if factor_diff <= 0:
+ break
+
+ current_factor = shift_factors_map.get(schedule.shift, 0)
+ if current_factor <= factor_diff:
+ self.depreciation_schedule.pop(i)
+ factor_diff -= current_factor
+ else:
+ new_factor = current_factor - factor_diff
+ self.depreciation_schedule[i].shift = reverse_shift_factors_map.get(new_factor)
+ factor_diff = 0
+
+ def add_depr_shifts(self, factor_diff, shift_factors_map, reverse_shift_factors_map):
+ factor_diff = abs(factor_diff)
+ shift_factors = sorted(shift_factors_map.values(), reverse=True)
+
+ while factor_diff > 0:
+ for factor in shift_factors:
+ if factor <= factor_diff:
+ self.add_schedule_row(factor, reverse_shift_factors_map)
+ factor_diff -= factor
+ break
+ else:
+ frappe.throw(
+ _("Could not find a suitable shift to match the difference: {0}").format(factor_diff)
+ )
+
+ def add_schedule_row(self, factor, reverse_shift_factors_map):
+ 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_shift_factors_map.get(factor),
+ },
+ )
+
+ def get_finance_book_row(self, asset_doc):
+ idx = 0
+ for d in asset_doc.get("finance_books"):
+ if d.finance_book == self.finance_book:
+ idx = d.idx
+ break
+
+ return asset_doc.get("finance_books")[idx - 1]
+
+ def modify_depr_schedule(self, temp_depr_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 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.modify_depr_schedule(self.asset_depr_schedule_doc.depreciation_schedule)
self.flags.ignore_validate = True
self.save()
@@ -83,143 +187,6 @@ class AssetShiftAllocation(Document):
)
)
- 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)
@@ -257,17 +224,3 @@ class AssetShiftAllocation(Document):
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_value_adjustment/asset_value_adjustment.js b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js
index 4435b2b1845..135fb76684e 100644
--- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js
+++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js
@@ -45,6 +45,9 @@ frappe.ui.form.on("Asset Value Adjustment", {
asset: function (frm) {
frm.trigger("set_acc_dimension");
+ if (frm.doc.asset) {
+ frm.trigger("set_current_asset_value");
+ }
},
finance_book: function (frm) {
diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.json b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.json
index e584c3d8202..e60d401055d 100644
--- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.json
+++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.json
@@ -54,8 +54,8 @@
"fieldname": "journal_entry",
"fieldtype": "Link",
"label": "Journal Entry",
- "options": "Journal Entry",
"no_copy": 1,
+ "options": "Journal Entry",
"read_only": 1
},
{
@@ -125,18 +125,18 @@
"fieldtype": "Column Break"
},
{
- "fieldname": "difference_account",
- "fieldtype": "Link",
- "label": "Difference Account",
- "no_copy": 1,
- "options": "Account",
- "reqd": 1
+ "fieldname": "difference_account",
+ "fieldtype": "Link",
+ "label": "Difference Account",
+ "no_copy": 1,
+ "options": "Account",
+ "reqd": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2024-08-13 16:21:18.639208",
+ "modified": "2024-12-18 15:04:18.726505",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Value Adjustment",
@@ -188,7 +188,6 @@
"write": 1
}
],
- "quick_entry": 1,
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
index 6766b827f7f..2ad6146ffa6 100644
--- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
+++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
@@ -5,7 +5,7 @@
import frappe
from frappe import _
from frappe.model.document import Document
-from frappe.utils import flt, formatdate, get_link_to_form, getdate
+from frappe.utils import cstr, flt, formatdate, get_link_to_form, getdate
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_checks_for_pl_and_bs_accounts,
@@ -14,7 +14,7 @@ from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciatio
from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
- make_new_active_asset_depr_schedules_and_cancel_current_ones,
+ reschedule_depreciation,
)
@@ -46,10 +46,26 @@ class AssetValueAdjustment(Document):
self.set_current_asset_value()
self.set_difference_amount()
+ def validate_date(self):
+ asset_purchase_date = frappe.db.get_value("Asset", self.asset, "purchase_date")
+ if getdate(self.date) < getdate(asset_purchase_date):
+ frappe.throw(
+ _("Asset Value Adjustment cannot be posted before Asset's purchase date {0}.").format(
+ formatdate(asset_purchase_date)
+ ),
+ title=_("Incorrect Date"),
+ )
+
+ def set_difference_amount(self):
+ self.difference_amount = flt(self.new_asset_value - self.current_asset_value)
+
+ def set_current_asset_value(self):
+ if not self.current_asset_value and self.asset:
+ self.current_asset_value = get_asset_value_after_depreciation(self.asset, self.finance_book)
+
def on_submit(self):
- self.make_depreciation_entry()
- self.set_value_after_depreciation()
- self.update_asset(self.new_asset_value)
+ self.make_asset_revaluation_entry()
+ self.update_asset()
add_asset_activity(
self.asset,
_("Asset's value adjusted after submission of Asset Value Adjustment {0}").format(
@@ -67,27 +83,7 @@ class AssetValueAdjustment(Document):
),
)
- def validate_date(self):
- asset_purchase_date = frappe.db.get_value("Asset", self.asset, "purchase_date")
- if getdate(self.date) < getdate(asset_purchase_date):
- frappe.throw(
- _("Asset Value Adjustment cannot be posted before Asset's purchase date {0}.").format(
- formatdate(asset_purchase_date)
- ),
- title=_("Incorrect Date"),
- )
-
- def set_difference_amount(self):
- self.difference_amount = flt(self.new_asset_value - self.current_asset_value)
-
- def set_value_after_depreciation(self):
- frappe.db.set_value("Asset", self.asset, "value_after_depreciation", self.new_asset_value)
-
- def set_current_asset_value(self):
- if not self.current_asset_value and self.asset:
- self.current_asset_value = get_asset_value_after_depreciation(self.asset, self.finance_book)
-
- def make_depreciation_entry(self):
+ def make_asset_revaluation_entry(self):
asset = frappe.get_doc("Asset", self.asset)
(
fixed_asset_account,
@@ -114,46 +110,15 @@ class AssetValueAdjustment(Document):
}
if self.difference_amount < 0:
- credit_entry = {
- "account": fixed_asset_account,
- "credit_in_account_currency": -self.difference_amount,
- **entry_template,
- }
- debit_entry = {
- "account": self.difference_account,
- "debit_in_account_currency": -self.difference_amount,
- **entry_template,
- }
+ credit_entry, debit_entry = self.get_entry_for_asset_value_decrease(
+ fixed_asset_account, entry_template
+ )
elif self.difference_amount > 0:
- credit_entry = {
- "account": self.difference_account,
- "credit_in_account_currency": self.difference_amount,
- **entry_template,
- }
- debit_entry = {
- "account": fixed_asset_account,
- "debit_in_account_currency": self.difference_amount,
- **entry_template,
- }
+ credit_entry, debit_entry = self.get_entry_for_asset_value_increase(
+ fixed_asset_account, entry_template
+ )
- accounting_dimensions = get_checks_for_pl_and_bs_accounts()
-
- for dimension in accounting_dimensions:
- if dimension.get("mandatory_for_bs"):
- credit_entry.update(
- {
- dimension["fieldname"]: self.get(dimension["fieldname"])
- or dimension.get("default_dimension")
- }
- )
-
- if dimension.get("mandatory_for_pl"):
- debit_entry.update(
- {
- dimension["fieldname"]: self.get(dimension["fieldname"])
- or dimension.get("default_dimension")
- }
- )
+ self.update_accounting_dimensions(credit_entry, debit_entry)
je.append("accounts", credit_entry)
je.append("accounts", debit_entry)
@@ -163,40 +128,81 @@ class AssetValueAdjustment(Document):
self.db_set("journal_entry", je.name)
- def update_asset(self, asset_value=None):
+ def get_entry_for_asset_value_decrease(self, fixed_asset_account, entry_template):
+ credit_entry = {
+ "account": fixed_asset_account,
+ "credit_in_account_currency": -self.difference_amount,
+ **entry_template,
+ }
+ debit_entry = {
+ "account": self.difference_account,
+ "debit_in_account_currency": -self.difference_amount,
+ **entry_template,
+ }
+
+ return credit_entry, debit_entry
+
+ def get_entry_for_asset_value_increase(self, fixed_asset_account, entry_template):
+ credit_entry = {
+ "account": self.difference_account,
+ "credit_in_account_currency": self.difference_amount,
+ **entry_template,
+ }
+ debit_entry = {
+ "account": fixed_asset_account,
+ "debit_in_account_currency": self.difference_amount,
+ **entry_template,
+ }
+
+ return credit_entry, debit_entry
+
+ def update_accounting_dimensions(self, credit_entry, debit_entry):
+ accounting_dimensions = get_checks_for_pl_and_bs_accounts()
+
+ for dimension in accounting_dimensions:
+ dimension_value = self.get(dimension["fieldname"]) or dimension.get("default_dimension")
+ if dimension.get("mandatory_for_bs"):
+ credit_entry.update({dimension["fieldname"]: dimension_value})
+
+ if dimension.get("mandatory_for_pl"):
+ debit_entry.update({dimension["fieldname"]: dimension_value})
+
+ def update_asset(self):
+ asset = self.update_asset_value_after_depreciation()
+ note = self.get_adjustment_note()
+ reschedule_depreciation(asset, note)
+
+ def update_asset_value_after_depreciation(self):
+ difference_amount = self.difference_amount if self.docstatus == 1 else -1 * self.difference_amount
+
asset = frappe.get_doc("Asset", self.asset)
+ if asset.calculate_depreciation:
+ for row in asset.finance_books:
+ if cstr(row.finance_book) == cstr(self.finance_book):
+ row.value_after_depreciation += flt(difference_amount)
+ row.db_update()
- if not asset.calculate_depreciation:
- asset.value_after_depreciation = asset_value
- asset.save()
- return
-
- asset.flags.decrease_in_asset_value_due_to_value_adjustment = True
+ asset.value_after_depreciation += flt(difference_amount)
+ asset.db_update()
+ return asset
+ def get_adjustment_note(self):
if self.docstatus == 1:
notes = _(
"This schedule was created when Asset {0} was adjusted through Asset Value Adjustment {1}."
).format(
- get_link_to_form("Asset", asset.name),
+ get_link_to_form("Asset", self.asset),
get_link_to_form(self.get("doctype"), self.get("name")),
)
elif self.docstatus == 2:
notes = _(
"This schedule was created when Asset {0}'s Asset Value Adjustment {1} was cancelled."
).format(
- get_link_to_form("Asset", asset.name),
+ get_link_to_form("Asset", self.asset),
get_link_to_form(self.get("doctype"), self.get("name")),
)
- make_new_active_asset_depr_schedules_and_cancel_current_ones(
- asset,
- notes,
- value_after_depreciation=asset_value,
- ignore_booked_entry=True,
- difference_amount=self.difference_amount,
- )
- asset.flags.ignore_validate_update_after_submit = True
- asset.save()
+ return notes
@frappe.whitelist()
diff --git a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py
index bfb34023089..e1d3b7b084f 100644
--- a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py
+++ b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py
@@ -114,12 +114,12 @@ class TestAssetValueAdjustment(IntegrationTestCase):
["2023-05-31", 9983.33, 45408.05],
["2023-06-30", 9983.33, 55391.38],
["2023-07-31", 9983.33, 65374.71],
- ["2023-08-31", 8300.0, 73674.71],
- ["2023-09-30", 8300.0, 81974.71],
- ["2023-10-31", 8300.0, 90274.71],
- ["2023-11-30", 8300.0, 98574.71],
- ["2023-12-31", 8300.0, 106874.71],
- ["2024-01-15", 8300.0, 115174.71],
+ ["2023-08-31", 9070.36, 74445.07],
+ ["2023-09-30", 9070.36, 83515.43],
+ ["2023-10-31", 9070.36, 92585.79],
+ ["2023-11-30", 9070.36, 101656.15],
+ ["2023-12-31", 9070.36, 110726.51],
+ ["2024-01-15", 4448.2, 115174.71],
]
schedules = [
@@ -154,7 +154,11 @@ class TestAssetValueAdjustment(IntegrationTestCase):
# create asset repair
asset_repair = create_asset_repair(
- asset=asset_doc, capitalize_repair_cost=1, item="_Test Non Stock Item", submit=1
+ asset=asset_doc,
+ capitalize_repair_cost=1,
+ item="_Test Non Stock Item",
+ submit=1,
+ increase_in_asset_life=1,
)
first_asset_depr_schedule = get_asset_depr_schedule_doc(asset_doc.name, "Active")
@@ -201,24 +205,24 @@ class TestAssetValueAdjustment(IntegrationTestCase):
["2023-05-31", 9983.33, 45408.05],
["2023-06-30", 9983.33, 55391.38],
["2023-07-31", 9983.33, 65374.71],
- ["2023-08-31", 2766.67, 68141.38],
- ["2023-09-30", 2766.67, 70908.05],
- ["2023-10-31", 2766.67, 73674.72],
- ["2023-11-30", 2766.67, 76441.39],
- ["2023-12-31", 2766.67, 79208.06],
- ["2024-01-31", 2766.67, 81974.73],
- ["2024-02-29", 2766.67, 84741.4],
- ["2024-03-31", 2766.67, 87508.07],
- ["2024-04-30", 2766.67, 90274.74],
- ["2024-05-31", 2766.67, 93041.41],
- ["2024-06-30", 2766.67, 95808.08],
- ["2024-07-31", 2766.67, 98574.75],
- ["2024-08-31", 2766.67, 101341.42],
- ["2024-09-30", 2766.67, 104108.09],
- ["2024-10-31", 2766.67, 106874.76],
- ["2024-11-30", 2766.67, 109641.43],
- ["2024-12-31", 2766.67, 112408.1],
- ["2025-01-15", 2766.61, 115174.71],
+ ["2023-08-31", 2847.27, 68221.98],
+ ["2023-09-30", 2847.27, 71069.25],
+ ["2023-10-31", 2847.27, 73916.52],
+ ["2023-11-30", 2847.27, 76763.79],
+ ["2023-12-31", 2847.27, 79611.06],
+ ["2024-01-31", 2847.27, 82458.33],
+ ["2024-02-29", 2847.27, 85305.6],
+ ["2024-03-31", 2847.27, 88152.87],
+ ["2024-04-30", 2847.27, 91000.14],
+ ["2024-05-31", 2847.27, 93847.41],
+ ["2024-06-30", 2847.27, 96694.68],
+ ["2024-07-31", 2847.27, 99541.95],
+ ["2024-08-31", 2847.27, 102389.22],
+ ["2024-09-30", 2847.27, 105236.49],
+ ["2024-10-31", 2847.27, 108083.76],
+ ["2024-11-30", 2847.27, 110931.03],
+ ["2024-12-31", 2847.27, 113778.3],
+ ["2025-01-31", 1396.41, 115174.71],
]
schedules = [
@@ -246,12 +250,12 @@ class TestAssetValueAdjustment(IntegrationTestCase):
["2023-05-31", 9983.33, 45408.05],
["2023-06-30", 9983.33, 55391.38],
["2023-07-31", 9983.33, 65374.71],
- ["2023-08-31", 8208.33, 73583.04],
- ["2023-09-30", 8208.33, 81791.37],
- ["2023-10-31", 8208.33, 89999.7],
- ["2023-11-30", 8208.33, 98208.03],
- ["2023-12-31", 8208.33, 106416.36],
- ["2024-01-15", 8208.35, 114624.71],
+ ["2023-08-31", 8970.18, 74344.89],
+ ["2023-09-30", 8970.18, 83315.07],
+ ["2023-10-31", 8970.18, 92285.25],
+ ["2023-11-30", 8970.18, 101255.43],
+ ["2023-12-31", 8970.18, 110225.61],
+ ["2024-01-15", 4399.1, 114624.71],
]
schedules = [
@@ -262,7 +266,7 @@ class TestAssetValueAdjustment(IntegrationTestCase):
self.assertEqual(schedules, expected_schedules)
def test_difference_amount(self):
- pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=120000.0, location="Test Location")
+ pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location")
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name")
asset_doc = frappe.get_doc("Asset", asset_name)
@@ -282,17 +286,18 @@ class TestAssetValueAdjustment(IntegrationTestCase):
)
asset_doc.submit()
+ current_asset_value = get_asset_value_after_depreciation(asset_doc.name)
adj_doc = make_asset_value_adjustment(
asset=asset_doc.name,
- current_asset_value=54000,
- new_asset_value=50000.0,
+ current_asset_value=current_asset_value,
+ new_asset_value=40000,
date="2023-08-21",
)
adj_doc.submit()
difference_amount = adj_doc.new_asset_value - adj_doc.current_asset_value
- self.assertEqual(difference_amount, -4000)
+ self.assertEqual(difference_amount, -60000)
asset_doc.load_from_db()
- self.assertEqual(asset_doc.value_after_depreciation, 50000.0)
+ self.assertEqual(asset_doc.finance_books[0].value_after_depreciation, 40000.0)
def make_asset_value_adjustment(**args):
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 1d9dc0607e0..76a9ab30f01 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -411,4 +411,5 @@ erpnext.patches.v15_0.update_payment_schedule_fields_in_invoices
erpnext.patches.v15_0.rename_group_by_to_categorize_by
execute:frappe.db.set_single_value("Accounts Settings", "receivable_payable_fetch_method", "Buffered Cursor")
erpnext.patches.v14_0.set_update_price_list_based_on
+erpnext.patches.v15_0.update_journal_entry_type
erpnext.patches.v15_0.set_grand_total_to_default_mop
diff --git a/erpnext/patches/v15_0/update_journal_entry_type.py b/erpnext/patches/v15_0/update_journal_entry_type.py
new file mode 100644
index 00000000000..0d243bac2f7
--- /dev/null
+++ b/erpnext/patches/v15_0/update_journal_entry_type.py
@@ -0,0 +1,19 @@
+import frappe
+
+
+def execute():
+ custom_je_type = frappe.db.get_value(
+ "Property Setter",
+ {"doc_type": "Journal Entry", "field_name": "voucher_type", "property": "options"},
+ ["name", "value"],
+ as_dict=True,
+ )
+ if custom_je_type:
+ custom_je_type.value += "\nAsset Disposal"
+ frappe.db.set_value("Property Setter", custom_je_type.name, "value", custom_je_type.value)
+
+ scrapped_journal_entries = frappe.get_all(
+ "Asset", filters={"journal_entry_for_scrap": ["is", "not set"]}, fields=["name"]
+ )
+ for je in scrapped_journal_entries:
+ frappe.db.set_value("Journal Entry", je.name, "voucher_type", "Asset Disposal")