refactor: Asset module code for better readability

refactor: Asset module code for better readability
This commit is contained in:
Khushi Rawat
2025-05-13 14:55:54 +05:30
committed by GitHub
31 changed files with 2954 additions and 2480 deletions

View File

@@ -89,7 +89,7 @@
"label": "Entry Type", "label": "Entry Type",
"oldfieldname": "voucher_type", "oldfieldname": "voucher_type",
"oldfieldtype": "Select", "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, "reqd": 1,
"search_index": 1 "search_index": 1
}, },
@@ -557,7 +557,7 @@
"table_fieldname": "payment_entries" "table_fieldname": "payment_entries"
} }
], ],
"modified": "2024-07-18 15:32:29.413598", "modified": "2024-12-26 15:32:20.730666",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Journal Entry", "name": "Journal Entry",

View File

@@ -100,6 +100,7 @@ class JournalEntry(AccountsController):
"Write Off Entry", "Write Off Entry",
"Opening Entry", "Opening Entry",
"Depreciation Entry", "Depreciation Entry",
"Asset Disposal",
"Exchange Rate Revaluation", "Exchange Rate Revaluation",
"Exchange Gain Or Loss", "Exchange Gain Or Loss",
"Deferred Revenue", "Deferred Revenue",
@@ -140,7 +141,7 @@ class JournalEntry(AccountsController):
self.validate_credit_debit_note() self.validate_credit_debit_note()
self.validate_empty_accounts_table() self.validate_empty_accounts_table()
self.validate_inter_company_accounts() 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_company_in_accounting_dimension()
self.validate_advance_accounts() self.validate_advance_accounts()
@@ -196,7 +197,6 @@ class JournalEntry(AccountsController):
self.update_asset_value() self.update_asset_value()
self.update_inter_company_jv() self.update_inter_company_jv()
self.update_invoice_discounting() self.update_invoice_discounting()
self.update_booked_depreciation()
def on_update_after_submit(self): def on_update_after_submit(self):
# Flag will be set on Reconciliation # Flag will be set on Reconciliation
@@ -232,7 +232,6 @@ class JournalEntry(AccountsController):
self.unlink_inter_company_jv() self.unlink_inter_company_jv()
self.unlink_asset_adjustment_entry() self.unlink_asset_adjustment_entry()
self.update_invoice_discounting() self.update_invoice_discounting()
self.update_booked_depreciation(1)
def get_title(self): def get_title(self):
return self.pay_to_recd_from or self.accounts[0].account 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")) frappe.throw(_("Total Credit/ Debit Amount should be same as linked Journal Entry"))
def validate_depr_entry_voucher_type(self): def validate_depr_account_and_depr_entry_voucher_type(self):
if ( for d in self.get("accounts"):
any(d.account_type == "Depreciation" for d in self.get("accounts")) if d.account_type == "Depreciation":
and self.voucher_type != "Depreciation Entry" if self.voucher_type != "Depreciation Entry":
): frappe.throw(
frappe.throw(_("Journal Entry type should be set as Depreciation Entry for asset depreciation")) _("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): def validate_stock_accounts(self):
stock_accounts = get_stock_accounts(self.company, accounts=self.accounts) stock_accounts = get_stock_accounts(self.company, accounts=self.accounts)
@@ -379,7 +382,11 @@ class JournalEntry(AccountsController):
self.remove(d) self.remove(d)
def update_asset_value(self): 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 return
for d in self.get("accounts"): for d in self.get("accounts"):
@@ -389,22 +396,59 @@ class JournalEntry(AccountsController):
and d.account_type == "Depreciation" and d.account_type == "Depreciation"
and d.debit and d.debit
): ):
asset = frappe.get_doc("Asset", d.reference_name) asset = frappe.get_cached_doc("Asset", d.reference_name)
if asset.calculate_depreciation: if asset.calculate_depreciation:
fb_idx = 1 self.update_journal_entry_link_on_depr_schedule(asset, d)
if self.finance_book: self.update_value_after_depreciation(asset, d.debit)
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)
asset.db_set("value_after_depreciation", asset.value_after_depreciation - d.debit)
asset.set_status() 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): def update_inter_company_jv(self):
if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference: if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference:
@@ -459,25 +503,6 @@ class JournalEntry(AccountsController):
if status: if status:
inv_disc_doc.set_status(status=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): def unlink_advance_entry_reference(self):
for d in self.get("accounts"): for d in self.get("accounts"):
if d.is_advance == "Yes" and d.reference_type in ("Sales Invoice", "Purchase Invoice"): 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 = asset.get("finance_books")[fb_idx - 1]
fb_row.value_after_depreciation += d.debit fb_row.value_after_depreciation += d.debit
fb_row.db_update() 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_status()
asset.set_total_booked_depreciations()
elif self.voucher_type == "Journal Entry" and d.reference_type == "Asset" and d.reference_name: elif self.voucher_type == "Journal Entry" and d.reference_type == "Asset" and d.reference_name:
journal_entry_for_scrap = frappe.db.get_value( journal_entry_for_scrap = frappe.db.get_value(
"Asset", d.reference_name, "journal_entry_for_scrap" "Asset", d.reference_name, "journal_entry_for_scrap"

View File

@@ -39,7 +39,7 @@ from erpnext.assets.doctype.asset.depreciation import (
get_gl_entries_on_asset_disposal, get_gl_entries_on_asset_disposal,
get_gl_entries_on_asset_regain, get_gl_entries_on_asset_regain,
reset_depreciation_schedule, 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_activity.asset_activity import add_asset_activity
from erpnext.controllers.accounts_controller import validate_account_head from erpnext.controllers.accounts_controller import validate_account_head
@@ -368,21 +368,34 @@ class SalesInvoice(SellingController):
validate_docs_for_deferred_accounting([self.name], []) validate_docs_for_deferred_accounting([self.name], [])
def validate_fixed_asset(self): def validate_fixed_asset(self):
for d in self.get("items"): if self.doctype != "Sales Invoice":
if d.is_fixed_asset and d.meta.get_field("asset") and d.asset: return
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"))
elif asset.status in ("Scrapped", "Cancelled", "Capitalized") or ( for d in self.get("items"):
asset.status == "Sold" and not self.is_return if d.is_fixed_asset:
): if d.asset:
frappe.throw( if not self.is_return:
_("Row #{0}: Asset {1} cannot be submitted, it is already {2}").format( asset_status = frappe.db.get_value("Asset", d.asset, "status")
d.idx, d.asset, 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): def validate_item_cost_centers(self):
for item in self.items: for item in self.items:
@@ -464,6 +477,8 @@ class SalesInvoice(SellingController):
self.update_stock_reservation_entries() self.update_stock_reservation_entries()
self.update_stock_ledger() self.update_stock_ledger()
self.process_asset_depreciation()
# this sequence because outstanding may get -ve # this sequence because outstanding may get -ve
self.make_gl_entries() self.make_gl_entries()
@@ -583,6 +598,8 @@ class SalesInvoice(SellingController):
if self.update_stock == 1: if self.update_stock == 1:
self.update_stock_ledger() self.update_stock_ledger()
self.process_asset_depreciation()
self.make_gl_entries_on_cancel() self.make_gl_entries_on_cancel()
if self.update_stock == 1: if self.update_stock == 1:
@@ -1253,6 +1270,90 @@ class SalesInvoice(SellingController):
): ):
throw(_("Delivery Note {0} is not submitted").format(d.delivery_note)) 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): def make_gl_entries(self, gl_entries=None, from_repost=False):
from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries 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(): if self.is_internal_transfer():
continue continue
if item.is_fixed_asset: if item.is_fixed_asset and item.asset:
asset = self.get_asset(item) self.get_gl_entries_for_fixed_asset(item, gl_entries)
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)
else: else:
income_account = ( income_account = (
item.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): if cint(self.update_stock) and erpnext.is_perpetual_inventory_enabled(self.company):
gl_entries += super().get_gl_entries() gl_entries += super().get_gl_entries()
def get_asset(self, item): def get_gl_entries_for_fixed_asset(self, item, gl_entries):
if item.get("asset"): asset = frappe.get_cached_doc("Asset", item.asset)
asset = frappe.get_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: else:
frappe.throw( fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
_("Row #{0}: You must select an Asset for Item {1}.").format(item.idx, item.item_name), asset,
title=_("Missing Asset"), item.base_net_amount,
item.finance_book,
self.get("doctype"),
self.get("name"),
self.get("posting_date"),
) )
self.check_finance_books(item, asset) for gle in fixed_asset_gl_entries:
return asset gle["against"] = self.customer
gl_entries.append(self.get_gl_dict(gle, item=item))
@property @property
def enable_discount_accounting(self): def enable_discount_accounting(self):
@@ -1539,12 +1598,6 @@ class SalesInvoice(SellingController):
return self._enable_discount_accounting 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): def make_loyalty_point_redemption_gle(self, gl_entries):
if cint(self.redeem_loyalty_points and self.loyalty_points and not self.is_consolidated): if cint(self.redeem_loyalty_points and self.loyalty_points and not self.is_consolidated):
gl_entries.append( gl_entries.append(

View File

@@ -2979,7 +2979,7 @@ class TestSalesInvoice(ERPNextTestSuite):
expected_values = [ expected_values = [
["2020-06-30", 1366.12, 1366.12], ["2020-06-30", 1366.12, 1366.12],
["2021-06-30", 20000.0, 21366.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")): for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")):
@@ -3011,7 +3011,7 @@ class TestSalesInvoice(ERPNextTestSuite):
) )
asset.load_from_db() 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")): for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")):
self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date) 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.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
self.assertTrue(schedule.journal_entry) 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): def test_sales_invoice_against_supplier(self):
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import ( from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
make_customer, make_customer,

View File

@@ -103,14 +103,26 @@ frappe.ui.form.on("Asset", {
}, },
__("Manage") __("Manage")
); );
} else if (frm.doc.status == "Scrapped") {
frm.add_custom_button( frm.add_custom_button(
__("Restore Asset"), __("Repair Asset"),
function () { function () {
erpnext.asset.restore_asset(frm); frm.trigger("create_asset_repair");
}, },
__("Manage") __("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) { if (frm.doc.maintenance_required && !frm.doc.maintenance_schedule) {
@@ -123,23 +135,7 @@ frappe.ui.form.on("Asset", {
); );
} }
frm.add_custom_button( if (["Submitted", "Partially Depreciated"].includes(frm.doc.status)) {
__("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") {
frm.add_custom_button( frm.add_custom_button(
__("Adjust Asset Value"), __("Adjust Asset Value"),
function () { function () {

View File

@@ -405,7 +405,6 @@
"read_only": 1 "read_only": 1
}, },
{ {
"collapsible": 1,
"collapsible_depends_on": "is_existing_asset", "collapsible_depends_on": "is_existing_asset",
"fieldname": "purchase_details_section", "fieldname": "purchase_details_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
@@ -429,6 +428,7 @@
"fieldname": "split_from", "fieldname": "split_from",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Split From", "label": "Split From",
"no_copy": 1,
"options": "Asset", "options": "Asset",
"read_only": 1 "read_only": 1
}, },
@@ -483,6 +483,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"default": "0",
"depends_on": "eval:doc.docstatus > 0", "depends_on": "eval:doc.docstatus > 0",
"fieldname": "additional_asset_cost", "fieldname": "additional_asset_cost",
"fieldtype": "Currency", "fieldtype": "Currency",

View File

@@ -33,9 +33,6 @@ from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_sched
convert_draft_asset_depr_schedules_into_active, convert_draft_asset_depr_schedules_into_active,
get_asset_depr_schedule_doc, get_asset_depr_schedule_doc,
get_depr_schedule, 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 from erpnext.controllers.accounts_controller import AccountsController
@@ -131,29 +128,60 @@ class Asset(AccountsController):
self.set_missing_values() self.set_missing_values()
self.validate_gross_and_purchase_amount() self.validate_gross_and_purchase_amount()
self.validate_finance_books() 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: def create_asset_depreciation_schedule(self):
self.prepare_depreciation_data() self.set_depr_rate_and_value_after_depreciation()
if self.calculate_depreciation: if self.split_from or not self.calculate_depreciation:
update_draft_asset_depr_schedules(self) return
if frappe.db.exists("Asset", self.name): schedules = []
asset_depr_schedules_names = make_draft_asset_depr_schedules_if_not_present(self) 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: schedule_doc = get_asset_depr_schedule_doc(self.name, "Draft", row.finance_book)
asset_depr_schedules_links = get_comma_separated_links( if not schedule_doc:
asset_depr_schedules_names, "Asset Depreciation Schedule" schedule_doc = frappe.new_doc("Asset Depreciation Schedule")
) schedule_doc.asset = self.name
frappe.msgprint( schedule_doc.create_depreciation_schedule(row)
_( schedule_doc.save()
"Asset Depreciation Schedules created:<br>{0}<br><br>Please check, edit if needed, and submit the Asset." schedules.append(schedule_doc.name)
).format(asset_depr_schedules_links)
) 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:<br>{0}<br><br>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.validate_expected_value_after_useful_life()
self.set_total_booked_depreciations() self.set_total_booked_depreciations()
self.total_asset_cost = self.gross_purchase_amount
self.status = self.get_status()
def on_submit(self): def on_submit(self):
self.validate_in_use_date() self.validate_in_use_date()
@@ -179,16 +207,6 @@ class Asset(AccountsController):
add_asset_activity(self.name, _("Asset cancelled")) add_asset_activity(self.name, _("Asset cancelled"))
def after_insert(self): 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:<br>{0}<br><br>Please check, edit if needed, and submit the Asset."
).format(asset_depr_schedules_links)
)
if ( if (
not frappe.db.exists( not frappe.db.exists(
{ {
@@ -250,16 +268,6 @@ class Asset(AccountsController):
if self.is_existing_asset and self.purchase_invoice: if self.is_existing_asset and self.purchase_invoice:
frappe.throw(_("Purchase Invoice cannot be made against an existing asset {0}").format(self.name)) 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): def validate_item(self):
item = frappe.get_cached_value( item = frappe.get_cached_value(
"Item", self.item_code, ["is_fixed_asset", "is_stock_item", "disabled"], as_dict=1 "Item", self.item_code, ["is_fixed_asset", "is_stock_item", "disabled"], as_dict=1
@@ -456,61 +464,65 @@ class Asset(AccountsController):
frappe.throw( frappe.throw(
_("Row {0}: Expected Value After Useful Life must be less than Gross Purchase Amount").format( _("Row {0}: Expected Value After Useful Life must be less than Gross Purchase Amount").format(
row.idx row.idx
), )
title=_("Invalid Schedule"),
) )
if not row.depreciation_start_date: 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) row.depreciation_start_date = get_last_day(self.available_for_use_date)
self.validate_depreciation_start_date(row)
if not self.is_existing_asset: if not self.is_existing_asset:
self.opening_accumulated_depreciation = 0 self.opening_accumulated_depreciation = 0
self.opening_number_of_booked_depreciations = 0 self.opening_number_of_booked_depreciations = 0
else: else:
depreciable_amount = flt( self.validate_opening_depreciation_values(row)
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
)
)
if self.opening_accumulated_depreciation: def validate_opening_depreciation_values(self, row):
if not self.opening_number_of_booked_depreciations: row.expected_value_after_useful_life = flt(
frappe.throw(_("Please set Opening Number of Booked Depreciations")) row.expected_value_after_useful_life, self.precision("gross_purchase_amount")
else: )
self.opening_number_of_booked_depreciations = 0 depreciable_amount = flt(
flt(self.gross_purchase_amount) - flt(row.expected_value_after_useful_life),
if flt(row.total_number_of_depreciations) <= cint(self.opening_number_of_booked_depreciations): self.precision("gross_purchase_amount"),
frappe.throw( )
_( if flt(self.opening_accumulated_depreciation) > depreciable_amount:
"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):
frappe.throw( frappe.throw(
_("Depreciation Row {0}: Next Depreciation Date cannot be before Purchase Date").format( _("Row #{0}: Opening Accumulated Depreciation must be less than or equal to {1}").format(
row.idx row.idx, depreciable_amount
) )
) )
if row.depreciation_start_date and getdate(row.depreciation_start_date) < getdate( if self.opening_accumulated_depreciation:
self.available_for_use_date 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( frappe.throw(
_( _(
"Depreciation Row {0}: Next Depreciation Date cannot be before Available-for-use Date" "Row #{0}: Total Number of Depreciations cannot be less than or equal to Opening Number of Booked Depreciations"
).format(row.idx) ).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): def set_total_booked_depreciations(self):
@@ -530,15 +542,11 @@ class Asset(AccountsController):
if not depr_schedule: if not depr_schedule:
continue continue
accumulated_depreciation_after_full_schedule = [ accumulated_depreciation_after_full_schedule = max(
d.accumulated_depreciation_amount for d in depr_schedule [d.accumulated_depreciation_amount for d in depr_schedule]
] )
if accumulated_depreciation_after_full_schedule: if accumulated_depreciation_after_full_schedule:
accumulated_depreciation_after_full_schedule = max(
accumulated_depreciation_after_full_schedule
)
asset_value_after_full_schedule = flt( asset_value_after_full_schedule = flt(
flt(self.gross_purchase_amount) - flt(accumulated_depreciation_after_full_schedule), flt(self.gross_purchase_amount) - flt(accumulated_depreciation_after_full_schedule),
self.precision("gross_purchase_amount"), self.precision("gross_purchase_amount"),
@@ -607,7 +615,10 @@ class Asset(AccountsController):
def get_status(self): def get_status(self):
"""Returns status based on whether it is draft, submitted, scrapped or depreciated""" """Returns status based on whether it is draft, submitted, scrapped or depreciated"""
if self.docstatus == 0: if self.docstatus == 0:
status = "Draft" if self.is_composite_asset:
status = "Work In Progress"
else:
status = "Draft"
elif self.docstatus == 1: elif self.docstatus == 1:
status = "Submitted" status = "Submitted"
@@ -624,13 +635,13 @@ class Asset(AccountsController):
].expected_value_after_useful_life ].expected_value_after_useful_life
value_after_depreciation = self.finance_books[idx].value_after_depreciation value_after_depreciation = self.finance_books[idx].value_after_depreciation
if ( if (
flt(value_after_depreciation) <= expected_value_after_useful_life flt(value_after_depreciation) <= expected_value_after_useful_life
or self.is_fully_depreciated or self.is_fully_depreciated
): ):
status = "Fully Depreciated" status = "Fully Depreciated"
elif flt(value_after_depreciation) < flt(self.gross_purchase_amount): elif flt(value_after_depreciation) < flt(self.gross_purchase_amount):
status = "Partially Depreciated" status = "Partially Depreciated"
elif self.docstatus == 2: elif self.docstatus == 2:
status = "Cancelled" status = "Cancelled"
return status return status
@@ -820,53 +831,52 @@ class Asset(AccountsController):
if isinstance(args, str): if isinstance(args, str):
args = json.loads(args) 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": 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("total_number_of_depreciations"), 2)
* flt(args.get("frequency_of_depreciation")) * flt(args.get("frequency_of_depreciation"))
) )
/ 12 / 12
) ),
rate_field_precision,
)
if args.get("depreciation_method") == "Written Down Value": def get_written_down_value_rate(self, args, rate_field_precision, on_validate):
if ( if args.get("rate_of_depreciation") and on_validate:
args.get("rate_of_depreciation") return 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")
if args.get("rate_of_depreciation") and not flt(args.get("expected_value_after_useful_life")): if args.get("rate_of_depreciation") and not flt(args.get("expected_value_after_useful_life")):
return args.get("rate_of_depreciation") return args.get("rate_of_depreciation")
if self.flags.increase_in_asset_value_due_to_repair: if flt(args.get("value_after_depreciation")):
value = flt(args.get("expected_value_after_useful_life")) / flt( current_asset_value = flt(args.get("value_after_depreciation"))
args.get("value_after_depreciation") else:
) current_asset_value = flt(self.gross_purchase_amount) - flt(self.opening_accumulated_depreciation)
else:
value = flt(args.get("expected_value_after_useful_life")) / (
flt(self.gross_purchase_amount) - flt(self.opening_accumulated_depreciation)
)
depreciation_rate = math.pow( value = flt(args.get("expected_value_after_useful_life")) / current_asset_value
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
),
)
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): def has_gl_entries(doctype, docname, target_account):
@@ -923,7 +933,7 @@ def get_asset_naming_series():
@frappe.whitelist() @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) asset_doc = frappe.get_doc("Asset", asset)
si = frappe.new_doc("Sales Invoice") si = frappe.new_doc("Sales Invoice")
si.company = company si.company = company
@@ -1185,166 +1195,203 @@ def get_values_from_purchase_doc(purchase_doc_name, item_code, doctype):
@frappe.whitelist() @frappe.whitelist()
def split_asset(asset_name, split_qty): 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) split_qty = cint(split_qty)
if split_qty >= asset.asset_quantity: validate_split_quantity(existing_asset, split_qty)
frappe.throw(_("Split qty cannot be grater than or equal to asset 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) return splitted_asset
update_existing_asset(asset, remaining_qty, new_asset.name)
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 return new_asset
def update_existing_asset(asset, remaining_qty, new_asset_name): def set_split_asset_values(asset_doc, scaling_factor, split_qty, existing_asset, is_new_asset):
remaining_gross_purchase_amount = flt( asset_doc.gross_purchase_amount = existing_asset.gross_purchase_amount * scaling_factor
(asset.gross_purchase_amount * remaining_qty) / asset.asset_quantity asset_doc.purchase_amount = existing_asset.gross_purchase_amount
) asset_doc.additional_asset_cost = existing_asset.additional_asset_cost * scaling_factor
opening_accumulated_depreciation = flt( asset_doc.total_asset_cost = asset_doc.gross_purchase_amount + asset_doc.additional_asset_cost
(asset.opening_accumulated_depreciation * remaining_qty) / asset.asset_quantity 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( for row in asset_doc.get("finance_books"):
"Asset", row.value_after_depreciation = row.value_after_depreciation * scaling_factor
asset.name, row.expected_value_after_useful_life = row.expected_value_after_useful_life * scaling_factor
{
"opening_accumulated_depreciation": opening_accumulated_depreciation,
"gross_purchase_amount": remaining_gross_purchase_amount,
"asset_quantity": remaining_qty,
},
)
add_asset_activity( if not is_new_asset:
asset.name, asset_doc.flags.ignore_validate_update_after_submit = True
_("Asset updated after being split into Asset {0}").format(get_link_to_form("Asset", new_asset_name)), asset_doc.save()
)
for row in asset.get("finance_books"):
value_after_depreciation = flt((row.value_after_depreciation * remaining_qty) / asset.asset_quantity) def log_asset_activity(existing_asset, asset_doc, splitted_asset, is_new_asset):
expected_value_after_useful_life = flt( if is_new_asset:
(row.expected_value_after_useful_life * remaining_qty) / asset.asset_quantity 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_doc.submit()
"Asset Finance Book", row.name, "value_after_depreciation", value_after_depreciation asset_doc.set_status()
) else:
frappe.db.set_value( add_asset_activity(
"Asset Finance Book", existing_asset.name,
row.name, _("Asset updated after being split into Asset {0}").format(
"expected_value_after_useful_life", get_link_to_form("Asset", splitted_asset.name)
expected_value_after_useful_life, ),
) )
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 # Add references in journal entries for new asset
if is_new_asset:
for term in new_asset_depr_schedule_doc.get("depreciation_schedule"): for row in new_asset.get("finance_books"):
depreciation_amount = flt((term.depreciation_amount * remaining_qty) / asset.asset_quantity) depr_schedule_doc = get_depr_schedule(new_asset.name, "Active", row.finance_book)
term.depreciation_amount = depreciation_amount for schedule in depr_schedule_doc:
accumulated_depreciation += depreciation_amount if schedule.journal_entry:
term.accumulated_depreciation_amount = accumulated_depreciation add_reference_in_jv_on_split(
schedule.journal_entry,
notes = _( new_asset.name,
"This schedule was created when Asset {0} was updated after being split into new Asset {1}." existing_asset.name,
).format(get_link_to_form(asset.doctype, asset.name), get_link_to_form(asset.doctype, new_asset_name)) schedule.depreciation_amount,
new_asset_depr_schedule_doc.notes = notes )
current_asset_depr_schedule_doc.flags.should_not_cancel_depreciation_entries = True
current_asset_depr_schedule_doc.cancel()
new_asset_depr_schedule_doc.submit()
def create_new_asset_after_split(asset, split_qty): def reschedule_depr_for_updated_asset(existing_asset, new_asset, fb_row, scaling_factor, is_new_asset):
new_asset = frappe.copy_doc(asset) """Reschedule depreciation for an asset after a split."""
new_gross_purchase_amount = flt((asset.gross_purchase_amount * split_qty) / asset.asset_quantity) current_depr_schedule_doc = get_asset_depr_schedule_doc(
opening_accumulated_depreciation = flt( existing_asset.name, "Active", fb_row.finance_book
(asset.opening_accumulated_depreciation * split_qty) / asset.asset_quantity )
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 update_depreciation_terms(new_depr_schedule_doc, scaling_factor)
if asset.purchase_amount: add_depr_schedule_notes(new_depr_schedule_doc, existing_asset, new_asset, is_new_asset)
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
for row in new_asset.get("finance_books"): if not is_new_asset:
row.value_after_depreciation = flt((row.value_after_depreciation * split_qty) / asset.asset_quantity) current_depr_schedule_doc.flags.should_not_cancel_depreciation_entries = True
row.expected_value_after_useful_life = flt( current_depr_schedule_doc.cancel()
(row.expected_value_after_useful_life * split_qty) / asset.asset_quantity
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( def add_depr_schedule_notes(new_depr_schedule_doc, existing_asset, new_asset, is_new_asset):
new_asset.name, notes = _("This schedule was created when Asset {0} was {1} into new Asset {2}.").format(
_("Asset created after being split from Asset {0}").format(get_link_to_form("Asset", asset.name)), 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_depr_schedule_doc.notes = notes
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
def add_reference_in_jv_on_split(entry_name, new_asset_name, old_asset_name, depreciation_amount): 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) journal_entry = frappe.get_doc("Journal Entry", entry_name)
entries_to_add = [] 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"): for account in journal_entry.get("accounts"):
if account.reference_name == old_asset_name: if account.reference_name == old_asset_name:
entries_to_add.append(frappe.copy_doc(account).as_dict()) entries_to_add.append(frappe.copy_doc(account).as_dict())
if account.credit: adjust_account_balance(account, depreciation_amount)
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
)
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: for entry in entries_to_add:
entry.reference_name = new_asset_name entry.reference_name = new_asset_name
if entry.credit: 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: elif entry.debit:
entry.debit = depreciation_amount entry.debit = depreciation_amount
entry.debit_in_account_currency = entry.exchange_rate * depreciation_amount entry.debit_in_account_currency = entry.exchange_rate * depreciation_amount
entry.idx = idx entry.idx = idx
idx += 1 idx += 1
journal_entry.append("accounts", entry) 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()

View File

@@ -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 ( from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_asset_depr_schedule_doc, get_asset_depr_schedule_doc,
get_asset_depr_schedule_name, get_asset_depr_schedule_name,
get_temp_asset_depr_schedule_doc, get_temp_depr_schedule_doc,
make_new_active_asset_depr_schedules_and_cancel_current_ones, reschedule_depreciation,
) )
@@ -40,72 +40,43 @@ def post_depreciation_entries(date=None):
): ):
return return
if not date: date = date or today()
date = today() book_depreciation_entries(date)
failed_asset_names = []
error_log_names = []
depreciable_asset_depr_schedules_data = get_depreciable_asset_depr_schedules_data(date) def book_depreciation_entries(date):
# Process depreciation entries for all depreciable assets
credit_and_debit_accounts_for_asset_category_and_company = {} failed_assets, error_logs = [], []
depreciation_cost_center_and_depreciation_series_for_company = (
get_depreciation_cost_center_and_depreciation_series_for_company()
)
depreciable_assets_data = get_depreciable_assets_data(date)
accounting_dimensions = get_checks_for_pl_and_bs_accounts() accounting_dimensions = get_checks_for_pl_and_bs_accounts()
for asset_depr_schedule_data in depreciable_asset_depr_schedules_data: for data in depreciable_assets_data:
( (depr_schedule_name, asset_name, sch_start_idx, sch_end_idx) = 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
),
}
)
try: try:
make_depreciation_entry( make_depreciation_entry(
asset_depr_schedule_name, depr_schedule_name,
date, date,
sch_start_idx, sch_start_idx,
sch_end_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, accounting_dimensions,
) )
frappe.db.commit() frappe.db.commit()
except Exception as e: except Exception as e:
frappe.db.rollback() frappe.db.rollback()
failed_asset_names.append(asset_name) failed_assets.append(asset_name)
error_log = frappe.log_error(e) error_log = frappe.log_error(e)
error_log_names.append(error_log.name) error_logs.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)
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() frappe.db.commit()
def get_depreciable_asset_depr_schedules_data(date): def get_depreciable_assets_data(date):
a = frappe.qb.DocType("Asset") a = frappe.qb.DocType("Asset")
ads = frappe.qb.DocType("Asset Depreciation Schedule") ads = frappe.qb.DocType("Asset Depreciation Schedule")
ds = frappe.qb.DocType("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) .on(ads.asset == a.name)
.join(ds) .join(ds)
.on(ads.name == ds.parent) .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.calculate_depreciation == 1)
.where(a.docstatus == 1) .where(a.docstatus == 1)
.where(ads.docstatus == 1) .where(ads.docstatus == 1)
@@ -136,10 +107,10 @@ def get_depreciable_asset_depr_schedules_data(date):
return res 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"): for row in asset_doc.get("finance_books"):
asset_depr_schedule_name = get_asset_depr_schedule_name(asset_doc.name, "Active", row.finance_book) depr_schedule_name = get_asset_depr_schedule_name(asset_doc.name, "Active", row.finance_book)
make_depreciation_entry(asset_depr_schedule_name, date) make_depreciation_entry(depr_schedule_name, disposal_date)
def get_acc_frozen_upto(): def get_acc_frozen_upto():
@@ -156,21 +127,26 @@ def get_acc_frozen_upto():
return return
def get_credit_and_debit_accounts_for_asset_category_and_company(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)
accumulated_depreciation_account,
depreciation_expense_account,
) = get_depreciation_accounts(asset_category, company)
credit_account, debit_account = get_credit_and_debit_accounts( 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) 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") company_names = frappe.db.get_all("Company", pluck="name")
res = {} res = {}
@@ -179,89 +155,70 @@ def get_depreciation_cost_center_and_depreciation_series_for_company():
depreciation_cost_center, depreciation_series = frappe.get_cached_value( depreciation_cost_center, depreciation_series = frappe.get_cached_value(
"Company", company_name, ["depreciation_cost_center", "series_for_depreciation_entry"] "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 return res
@frappe.whitelist() @frappe.whitelist()
def make_depreciation_entry( def make_depreciation_entry(
asset_depr_schedule_name, depr_schedule_name,
date=None, date=None,
sch_start_idx=None, sch_start_idx=None,
sch_end_idx=None, sch_end_idx=None,
credit_and_debit_accounts=None,
depreciation_cost_center_and_depreciation_series=None,
accounting_dimensions=None, accounting_dimensions=None,
): ):
frappe.has_permission("Journal Entry", throw=True) frappe.has_permission("Journal Entry", throw=True)
date = date or today()
if not date: depr_schedule_doc = frappe.get_doc("Asset Depreciation Schedule", depr_schedule_name)
date = today() 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) 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")))
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"))
]: ]:
try: try:
_make_journal_entry_for_depreciation( _make_journal_entry_for_depreciation(
asset_depr_schedule_doc, depr_schedule_doc,
asset, asset,
date, date,
d, d,
sch_start_idx, sch_start_idx,
sch_end_idx, sch_end_idx,
depreciation_cost_center, depr_cost_center,
depreciation_series, depr_series,
credit_account, credit_account,
debit_account, debit_account,
accounting_dimensions, accounting_dimensions,
) )
except Exception as e: except Exception as e:
depreciation_posting_error = e depr_posting_error = e
asset.reload()
asset.set_status() asset.set_status()
if not depreciation_posting_error: if not depr_posting_error:
asset.db_set("depr_entry_posting_status", "Successful") 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( def _make_journal_entry_for_depreciation(
asset_depr_schedule_doc, depr_schedule_doc,
asset, asset,
date, date,
depr_schedule, depr_schedule,
sch_start_idx, sch_start_idx,
sch_end_idx, sch_end_idx,
depreciation_cost_center, depr_cost_center,
depreciation_series, depr_series,
credit_account, credit_account,
debit_account, debit_account,
accounting_dimensions, accounting_dimensions,
@@ -272,19 +229,40 @@ def _make_journal_entry_for_depreciation(
return return
je = frappe.new_doc("Journal Entry") 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.voucher_type = "Depreciation Entry"
je.naming_series = depreciation_series je.naming_series = depr_series
je.posting_date = depr_schedule.schedule_date je.posting_date = depr_schedule.schedule_date
je.company = asset.company 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}" 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 = { credit_entry = {
"account": credit_account, "account": credit_account,
"credit_in_account_currency": depr_schedule.depreciation_amount, "credit_in_account_currency": depr_schedule.depreciation_amount,
"reference_type": "Asset", "reference_type": "Asset",
"reference_name": asset.name, "reference_name": asset.name,
"cost_center": depreciation_cost_center, "cost_center": depr_cost_center,
} }
debit_entry = { debit_entry = {
@@ -292,42 +270,20 @@ def _make_journal_entry_for_depreciation(
"debit_in_account_currency": depr_schedule.depreciation_amount, "debit_in_account_currency": depr_schedule.depreciation_amount,
"reference_type": "Asset", "reference_type": "Asset",
"reference_name": asset.name, "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"): if asset.get(dimension["fieldname"]) or dimension.get("mandatory_for_bs"):
credit_entry.update( credit_entry[dimension["fieldname"]] = asset.get(dimension["fieldname"]) or dimension.get(
{ "default_dimension"
dimension["fieldname"]: asset.get(dimension["fieldname"])
or dimension.get("default_dimension")
}
) )
if asset.get(dimension["fieldname"]) or dimension.get("mandatory_for_pl"): if asset.get(dimension["fieldname"]) or dimension.get("mandatory_for_pl"):
debit_entry.update( debit_entry[dimension["fieldname"]] = asset.get(dimension["fieldname"]) or dimension.get(
{ "default_dimension"
dimension["fieldname"]: asset.get(dimension["fieldname"])
or dimension.get("default_dimension")
}
) )
return credit_entry, debit_entry
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()
def get_depreciation_accounts(asset_category, company): 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") asset_links = get_comma_separated_links(failed_asset_names, "Asset")
error_log_links = get_comma_separated_links(error_log_names, "Error Log") error_log_links = get_comma_separated_links(error_log_names, "Error Log")
message = ( message = get_message_for_depr_entry_posting_error(asset_links, error_log_links)
_("Hello,")
+ "<br><br>"
+ _("The following assets have failed to automatically post depreciation entries: {0}").format(
asset_links
)
+ "."
+ "<br><br>"
+ _("Here are the error logs for the aforementioned failed depreciation entries: {0}").format(
error_log_links
)
+ "."
+ "<br><br>"
+ _("Please share this email with your support team so that they can find and fix the issue.")
)
frappe.sendmail(recipients=recipients, subject=subject, message=message) frappe.sendmail(recipients=recipients, subject=subject, message=message)
@@ -430,197 +372,184 @@ def get_comma_separated_links(names, doctype):
return links return links
def get_message_for_depr_entry_posting_error(asset_links, error_log_links):
return (
_("Hello,")
+ "<br><br>"
+ _("The following assets have failed to automatically post depreciation entries: {0}").format(
asset_links
)
+ "."
+ "<br><br>"
+ _("Here are the error logs for the aforementioned failed depreciation entries: {0}").format(
error_log_links
)
+ "."
+ "<br><br>"
+ _("Please share this email with your support team so that they can find and fix the issue.")
)
@frappe.whitelist() @frappe.whitelist()
def scrap_asset(asset_name, scrap_date=None): def scrap_asset(asset_name, scrap_date=None):
asset = frappe.get_doc("Asset", asset_name) 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: if asset.docstatus != 1:
frappe.throw(_("Asset {0} must be submitted").format(asset.name)) frappe.throw(_("Asset {0} must be submitted").format(asset.name))
elif asset.status in ("Cancelled", "Sold", "Scrapped", "Capitalized"): 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)) frappe.throw(_("Asset {0} cannot be scrapped, as it is already {1}").format(asset.name, asset.status))
today_date = getdate(today()) validate_scrap_date(asset, scrap_date)
date = getdate(scrap_date) or today_date
purchase_date = getdate(asset.purchase_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) 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") depreciation_series = frappe.get_cached_value("Company", asset.company, "series_for_depreciation_entry")
je = frappe.new_doc("Journal Entry") je = frappe.new_doc("Journal Entry")
je.voucher_type = "Journal Entry" je.voucher_type = "Asset Disposal"
je.naming_series = depreciation_series je.naming_series = depreciation_series
je.posting_date = date je.posting_date = scrap_date
je.company = asset.company 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): for entry in get_gl_entries_on_asset_disposal(asset, scrap_date):
entry.update({"reference_type": "Asset", "reference_name": asset_name}) entry.update({"reference_type": "Asset", "reference_name": asset.name})
je.append("accounts", entry) je.append("accounts", entry)
je.flags.ignore_permissions = True 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) add_asset_activity(asset.name, _("Asset scrapped"))
frappe.db.set_value("Asset", asset_name, "journal_entry_for_scrap", je.name) frappe.msgprint(
asset.set_status("Scrapped") _("Asset scrapped via Journal Entry {0}").format(get_link_to_form("Journal Entry", je.name))
)
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."))
@frappe.whitelist() @frappe.whitelist()
def restore_asset(asset_name): def restore_asset(asset_name):
asset = frappe.get_doc("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 def get_note_for_restore(asset):
return _("This schedule was created when Asset {0} was restored.").format(
notes = _("This schedule was created when Asset {0} was restored.").format(
get_link_to_form(asset.doctype, asset.name) get_link_to_form(asset.doctype, asset.name)
) )
reset_depreciation_schedule(asset, asset.disposal_date, notes)
asset.db_set("disposal_date", None) def cancel_journal_entry_for_scrap(asset):
asset.db_set("journal_entry_for_scrap", None) if asset.journal_entry_for_scrap:
je = asset.journal_entry_for_scrap
frappe.get_doc("Journal Entry", je).cancel() asset.db_set("disposal_date", None)
asset.db_set("journal_entry_for_scrap", None)
asset.set_status() frappe.get_doc("Journal Entry", je).cancel()
add_asset_activity(asset_name, _("Asset restored"))
def depreciate_asset(asset_doc, date, notes): def depreciate_asset(asset_doc, date, notes):
if not asset_doc.calculate_depreciation: if not asset_doc.calculate_depreciation:
return return
asset_doc.flags.ignore_validate_update_after_submit = True reschedule_depreciation(asset_doc, notes, disposal_date=date)
make_depreciation_entry_on_disposal(asset_doc, date)
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)
# 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() asset_doc.reload()
cancel_depreciation_entries(asset_doc, date) cancel_depreciation_entries(asset_doc, date)
@erpnext.allow_regional @erpnext.allow_regional
def cancel_depreciation_entries(asset_doc, date): 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 pass
def reset_depreciation_schedule(asset_doc, date, notes): def reset_depreciation_schedule(asset_doc, notes):
if not asset_doc.calculate_depreciation: if asset_doc.calculate_depreciation:
return reschedule_depreciation(asset_doc, notes)
asset_doc.set_total_booked_depreciations()
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 modify_depreciation_schedule_for_asset_repairs(asset, notes): def reverse_depreciation_entry_made_on_disposal(asset):
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):
for row in asset.get("finance_books"): for row in asset.get("finance_books"):
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active", row.finance_book) 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"): if not schedule_doc or not schedule_doc.get("depreciation_schedule"):
continue continue
for schedule_idx, schedule in enumerate(asset_depr_schedule_doc.get("depreciation_schedule")): for schedule_idx, schedule in enumerate(schedule_doc.get("depreciation_schedule")):
if schedule.schedule_date == date and schedule.journal_entry: if schedule.schedule_date == asset.disposal_date and schedule.journal_entry:
if not disposal_was_made_on_original_schedule_date( if not disposal_was_made_on_original_schedule_date(
schedule_idx, row, date schedule_idx, row, asset.disposal_date
) or disposal_happens_in_the_future(date): ) or disposal_happens_in_the_future(asset.disposal_date):
reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry) je = create_reverse_depreciation_entry(asset.name, schedule.journal_entry)
reverse_journal_entry.posting_date = nowdate() update_value_after_depreciation_on_asset_restore(schedule, row, je)
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()
def get_depreciation_amount_in_je(journal_entry): def disposal_was_made_on_original_schedule_date(schedule_idx, row, disposal_date):
if journal_entry.accounts[0].debit_in_account_currency: """
return journal_entry.accounts[0].debit_in_account_currency If asset is scrapped or sold on original schedule date,
else: then the depreciation entry should not be reversed.
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):
orginal_schedule_date = add_months( orginal_schedule_date = add_months(
row.depreciation_start_date, schedule_idx * cint(row.frequency_of_depreciation) 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): if is_last_day_of_the_month(row.depreciation_start_date):
orginal_schedule_date = get_last_day(orginal_schedule_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 True
return False return False
def disposal_happens_in_the_future(posting_date_of_disposal): def disposal_happens_in_the_future(disposal_date):
if posting_date_of_disposal > getdate(): if disposal_date > getdate():
return True return True
return False 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( def get_gl_entries_on_asset_regain(
asset, selling_amount=0, finance_book=None, voucher_type=None, voucher_no=None, date=None 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] row = asset_doc.finance_books[idx - 1]
temp_asset_depreciation_schedule = get_temp_asset_depr_schedule_doc( temp_asset_depreciation_schedule = get_temp_depr_schedule_doc(asset_doc, row, getdate(disposal_date))
asset_doc, row, getdate(disposal_date)
)
accumulated_depr_amount = temp_asset_depreciation_schedule.get("depreciation_schedule")[ accumulated_depr_amount = temp_asset_depreciation_schedule.get("depreciation_schedule")[
-1 -1

View File

@@ -30,11 +30,8 @@ from erpnext.assets.doctype.asset.depreciation import (
scrap_asset, scrap_asset,
) )
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( 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_asset_depr_schedule_doc,
get_depr_schedule, get_depr_schedule,
get_depreciation_amount,
) )
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
make_purchase_invoice as make_invoice, make_purchase_invoice as make_invoice,
@@ -196,7 +193,7 @@ class TestAsset(AssetSetup):
self.assertEqual(doc.items[0].is_fixed_asset, 1) self.assertEqual(doc.items[0].is_fixed_asset, 1)
def test_scrap_asset(self): def test_scrap_asset(self):
date = nowdate() date = "2025-05-05"
purchase_date = add_months(get_first_day(date), -2) purchase_date = add_months(get_first_day(date), -2)
asset = create_asset( asset = create_asset(
@@ -246,7 +243,7 @@ class TestAsset(AssetSetup):
frappe.ValidationError, scrap_asset, asset.name, scrap_date=before_last_booked_depreciation_date 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() asset.load_from_db()
first_asset_depr_schedule.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.gross_purchase_amount - asset.finance_books[0].value_after_depreciation,
asset.precision("gross_purchase_amount"), asset.precision("gross_purchase_amount"),
) )
pro_rata_amount, _, _ = _get_pro_rata_amt(
asset.finance_books[0], second_asset_depr_schedule.depreciation_amount = 9006.17
9000, 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), add_days(get_last_day(add_months(purchase_date, 1)), 1),
date, 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")) pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount"))
self.assertEqual( self.assertEqual(
@@ -331,7 +332,7 @@ class TestAsset(AssetSetup):
si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company") si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company")
si.customer = "_Test Customer" si.customer = "_Test Customer"
si.due_date = nowdate() si.due_date = date
si.get("items")[0].rate = 25000 si.get("items")[0].rate = 25000
si.insert() si.insert()
si.submit() si.submit()
@@ -344,19 +345,17 @@ class TestAsset(AssetSetup):
self.assertEqual(second_asset_depr_schedule.status, "Active") self.assertEqual(second_asset_depr_schedule.status, "Active")
self.assertEqual(first_asset_depr_schedule.status, "Cancelled") self.assertEqual(first_asset_depr_schedule.status, "Cancelled")
pro_rata_amount, _, _ = _get_pro_rata_amt( asset.load_from_db()
asset.finance_books[0], accumulated_depr_amount = flt(
9000, asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation,
add_days(get_last_day(add_months(purchase_date, 1)), 1), asset.precision("gross_purchase_amount"),
date,
original_schedule_date=get_last_day(nowdate()),
) )
pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount")) pro_rata_amount = flt(accumulated_depr_amount - 18000)
expected_gle = ( expected_gle = (
( (
"_Test Accumulated Depreciations - _TC", "_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, 0.0,
), ),
("_Test Fixed Asset - _TC", 0.0, 100000.0), ("_Test Fixed Asset - _TC", 0.0, 100000.0),
@@ -476,26 +475,26 @@ class TestAsset(AssetSetup):
asset = create_asset( asset = create_asset(
calculate_depreciation=1, calculate_depreciation=1,
asset_quantity=10, asset_quantity=10,
available_for_use_date="2020-01-01", available_for_use_date="2023-01-01",
purchase_date="2020-01-01", purchase_date="2023-01-01",
expected_value_after_useful_life=0, expected_value_after_useful_life=0,
total_number_of_depreciations=6, total_number_of_depreciations=6,
opening_number_of_booked_depreciations=1, opening_number_of_booked_depreciations=1,
frequency_of_depreciation=10, frequency_of_depreciation=12,
depreciation_start_date="2021-01-01", depreciation_start_date="2024-03-31",
opening_accumulated_depreciation=20000, opening_accumulated_depreciation=493.15,
gross_purchase_amount=120000, gross_purchase_amount=12000,
submit=1, submit=1,
) )
first_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active") first_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active")
self.assertEqual(first_asset_depr_schedule.status, "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.asset_quantity, 10)
self.assertEqual(asset.gross_purchase_amount, 120000) self.assertEqual(asset.gross_purchase_amount, 12000)
self.assertEqual(asset.opening_accumulated_depreciation, 20000) self.assertEqual(asset.opening_accumulated_depreciation, 493.15)
new_asset = split_asset(asset.name, 2) new_asset = split_asset(asset.name, 2)
asset.load_from_db() 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") 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.asset_quantity, 2)
self.assertEqual(new_asset.gross_purchase_amount, 24000) self.assertEqual(new_asset.gross_purchase_amount, 2400)
self.assertEqual(new_asset.opening_accumulated_depreciation, 4000) self.assertEqual(new_asset.opening_accumulated_depreciation, 98.63)
self.assertEqual(new_asset.split_from, asset.name) 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[0].depreciation_amount, 400)
self.assertEqual(depr_schedule_of_new_asset[1].depreciation_amount, 4000) self.assertEqual(depr_schedule_of_new_asset[1].depreciation_amount, 400)
self.assertEqual(asset.asset_quantity, 8) self.assertEqual(asset.asset_quantity, 8)
self.assertEqual(asset.gross_purchase_amount, 96000) self.assertEqual(asset.gross_purchase_amount, 9600)
self.assertEqual(asset.opening_accumulated_depreciation, 16000) self.assertEqual(asset.opening_accumulated_depreciation, 394.52)
self.assertEqual(depr_schedule_of_asset[0].depreciation_amount, 16000) self.assertEqual(depr_schedule_of_asset[0].depreciation_amount, 1600)
self.assertEqual(depr_schedule_of_asset[1].depreciation_amount, 16000) self.assertEqual(depr_schedule_of_asset[1].depreciation_amount, 1600)
journal_entry = depr_schedule_of_asset[0].journal_entry journal_entry = depr_schedule_of_asset[0].journal_entry
jv = frappe.get_doc("Journal Entry", journal_entry) jv = frappe.get_doc("Journal Entry", journal_entry)
self.assertEqual(jv.accounts[0].credit_in_account_currency, 16000) self.assertEqual(jv.accounts[0].credit_in_account_currency, 1600)
self.assertEqual(jv.accounts[1].debit_in_account_currency, 16000) self.assertEqual(jv.accounts[1].debit_in_account_currency, 1600)
self.assertEqual(jv.accounts[2].credit_in_account_currency, 4000) self.assertEqual(jv.accounts[2].credit_in_account_currency, 400)
self.assertEqual(jv.accounts[3].debit_in_account_currency, 4000) self.assertEqual(jv.accounts[3].debit_in_account_currency, 400)
self.assertEqual(jv.accounts[0].reference_name, asset.name) self.assertEqual(jv.accounts[0].reference_name, asset.name)
self.assertEqual(jv.accounts[1].reference_name, asset.name) self.assertEqual(jv.accounts[1].reference_name, asset.name)
@@ -944,12 +943,12 @@ class TestDepreciationMethods(AssetSetup):
) )
expected_schedules = [ expected_schedules = [
["2022-02-28", 310.89, 310.89], ["2022-02-28", 337.72, 337.72],
["2022-03-31", 654.45, 965.34], ["2022-03-31", 675.45, 1013.17],
["2022-04-30", 654.45, 1619.79], ["2022-04-30", 675.45, 1688.62],
["2022-05-31", 654.45, 2274.24], ["2022-05-31", 675.45, 2364.07],
["2022-06-30", 654.45, 2928.69], ["2022-06-30", 675.45, 3039.52],
["2022-07-15", 2071.31, 5000.0], ["2022-07-15", 1960.48, 5000.0],
] ]
schedules = [ schedules = [
@@ -1024,12 +1023,17 @@ class TestDepreciationBasics(AssetSetup):
"depreciation_start_date": "2020-12-31", "depreciation_start_date": "2020-12-31",
}, },
) )
asset.submit()
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active") 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( depreciation_amount = asset_depr_schedule_doc.get_depreciation_amount(0)
asset_depr_schedule_doc, asset, 100000, 100000, asset.finance_books[0]
)
self.assertEqual(depreciation_amount, 30000) self.assertEqual(depreciation_amount, 30000)
def test_make_depr_schedule(self): def test_make_depr_schedule(self):
@@ -1074,7 +1078,7 @@ class TestDepreciationBasics(AssetSetup):
def test_check_is_pro_rata(self): 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).""" """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.calculate_depreciation = 1
asset.append( asset.append(
@@ -1087,9 +1091,15 @@ class TestDepreciationBasics(AssetSetup):
"depreciation_start_date": "2020-12-31", "depreciation_start_date": "2020-12-31",
}, },
) )
asset.save()
has_pro_rata = _check_is_pro_rata(asset, asset.finance_books[0]) depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Draft")
self.assertFalse(has_pro_rata) 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.finance_books = []
asset.append( asset.append(
@@ -1102,9 +1112,15 @@ class TestDepreciationBasics(AssetSetup):
"depreciation_start_date": "2020-07-01", "depreciation_start_date": "2020-07-01",
}, },
) )
asset.save()
has_pro_rata = _check_is_pro_rata(asset, asset.finance_books[0]) depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Draft")
self.assertTrue(has_pro_rata) 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): 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).""" """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"]) self.assertFalse(entry["debit"])
def test_depr_entry_posting_when_depr_expense_account_is_an_income_account(self): 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 = frappe.get_doc("Account", "_Test Depreciations - _TC")
depr_expense_account.root_type = "Income" depr_expense_account.root_type = "Income"
depr_expense_account.parent_account = "Income - _TC" depr_expense_account.parent_account = "Income - _TC"
@@ -1303,26 +1317,20 @@ class TestDepreciationBasics(AssetSetup):
submit=1, submit=1,
) )
post_depreciation_entries(date="2021-06-01") jv = make_journal_entry(
asset.load_from_db() "_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) with self.assertRaises(frappe.ValidationError):
accounting_entries = [ jv.insert()
{"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()
def test_clear_depr_schedule(self): def test_clear_depr_schedule(self):
"""Tests if clear_depr_schedule() works as expected.""" """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 = 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) self.assertEqual(len(asset_depr_schedule_doc.get("depreciation_schedule")), 1)
@@ -1390,15 +1398,15 @@ class TestDepreciationBasics(AssetSetup):
asset.load_from_db() 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 = 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) 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 = 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) 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 = 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) self.assertEqual(len(asset_depr_schedule_doc_3.get("depreciation_schedule")), 0)
def test_depreciation_schedules_are_set_up_for_multiple_finance_books(self): def test_depreciation_schedules_are_set_up_for_multiple_finance_books(self):
@@ -1448,6 +1456,11 @@ class TestDepreciationBasics(AssetSetup):
submit=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()
post_depreciation_entries(date="2021-01-01") post_depreciation_entries(date="2021-01-01")
asset.load_from_db() asset.load_from_db()
@@ -1519,7 +1532,7 @@ class TestDepreciationBasics(AssetSetup):
) )
self.assertSequenceEqual(gle, expected_gle) 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): def test_expected_value_change(self):
""" """
@@ -1616,6 +1629,10 @@ class TestDepreciationBasics(AssetSetup):
frequency_of_depreciation=1, frequency_of_depreciation=1,
submit=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.status, "Submitted")
self.assertEqual(asset.get_value_after_depreciation(), 100000) self.assertEqual(asset.get_value_after_depreciation(), 100000)

View File

@@ -16,7 +16,7 @@ from erpnext.assets.doctype.asset.depreciation import (
get_gl_entries_on_asset_disposal, get_gl_entries_on_asset_disposal,
get_value_after_depreciation_on_disposal_date, get_value_after_depreciation_on_disposal_date,
reset_depreciation_schedule, 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_activity.asset_activity import add_asset_activity
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account 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.make_gl_entries()
self.repost_future_sle_and_gle() self.repost_future_sle_and_gle()
self.restore_consumed_asset_items() self.restore_consumed_asset_items()
self.update_target_asset()
def set_title(self): def set_title(self):
self.title = self.target_asset_name or self.target_item_name or self.target_item_code self.title = self.target_asset_name or self.target_item_name or self.target_item_code
@@ -602,13 +603,18 @@ class AssetCapitalization(StockController):
return return
total_target_asset_value = flt(self.total_value, self.precision("total_value")) total_target_asset_value = flt(self.total_value, self.precision("total_value"))
asset_doc = frappe.get_doc("Asset", self.target_asset) 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 if self.docstatus == 2:
asset_doc.set_status("Work In Progress") gross_purchase_amount = asset_doc.gross_purchase_amount - total_target_asset_value
asset_doc.flags.ignore_validate = True purchase_amount = asset_doc.purchase_amount - total_target_asset_value
asset_doc.save() 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( frappe.msgprint(
_("Asset {0} has been updated. Please set the depreciation details if any and submit it.").format( _("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): def restore_consumed_asset_items(self):
for item in self.asset_items: for item in self.asset_items:
asset = frappe.get_doc("Asset", item.asset) asset = frappe.get_doc("Asset", item.asset)
asset.db_set("disposal_date", None)
self.set_consumed_asset_status(asset) self.set_consumed_asset_status(asset)
if asset.calculate_depreciation: if asset.calculate_depreciation:
reverse_depreciation_entry_made_after_disposal(asset, self.posting_date) reverse_depreciation_entry_made_on_disposal(asset)
notes = _( notes = _(
"This schedule was created when Asset {0} was restored on Asset Capitalization {1}'s cancellation." "This schedule was created when Asset {0} was restored on Asset Capitalization {1}'s cancellation."
).format( ).format(
get_link_to_form(asset.doctype, asset.name), get_link_to_form(self.doctype, self.name) 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): def set_consumed_asset_status(self, asset):
if self.docstatus == 1: if self.docstatus == 1:

View File

@@ -31,7 +31,7 @@ frappe.ui.form.on("Depreciation Schedule", {
frappe.call({ frappe.call({
method: "erpnext.assets.doctype.asset.depreciation.make_depreciation_entry", method: "erpnext.assets.doctype.asset.depreciation.make_depreciation_entry",
args: { args: {
asset_depr_schedule_name: frm.doc.name, depr_schedule_name: frm.doc.name,
date: row.schedule_date, date: row.schedule_date,
}, },
debounce: 1000, debounce: 1000,

View File

@@ -25,6 +25,7 @@
"column_break_8", "column_break_8",
"frequency_of_depreciation", "frequency_of_depreciation",
"expected_value_after_useful_life", "expected_value_after_useful_life",
"value_after_depreciation",
"depreciation_schedule_section", "depreciation_schedule_section",
"depreciation_schedule", "depreciation_schedule",
"details_section", "details_section",
@@ -38,6 +39,7 @@
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
"label": "Asset", "label": "Asset",
"link_filters": "[[\"Asset\",\"docstatus\",\"<\",\"2\"],[\"Asset\",\"company\",\"=\",\"eval:doc.company\"]]",
"options": "Asset", "options": "Asset",
"reqd": 1 "reqd": 1
}, },
@@ -202,12 +204,18 @@
"label": "Company", "label": "Company",
"options": "Company", "options": "Company",
"read_only": 1 "read_only": 1
},
{
"fieldname": "value_after_depreciation",
"fieldtype": "Currency",
"label": "Value After Depreciation",
"read_only": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2024-03-27 13:06:34.135004", "modified": "2024-12-02 17:54:20.635668",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset Depreciation Schedule", "name": "Asset Depreciation Schedule",

View File

@@ -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

View File

@@ -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

View File

@@ -3,8 +3,9 @@
import frappe import frappe
from frappe.tests import IntegrationTestCase, UnitTestCase 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 ( from erpnext.assets.doctype.asset.depreciation import (
post_depreciation_entries, post_depreciation_entries,
) )
@@ -13,6 +14,10 @@ from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_sched
get_asset_depr_schedule_doc, get_asset_depr_schedule_doc,
get_depr_schedule, 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): class UnitTestAssetDepreciationSchedule(UnitTestCase):
@@ -41,6 +46,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
self.assertRaises(frappe.ValidationError, second_asset_depr_schedule.insert) self.assertRaises(frappe.ValidationError, second_asset_depr_schedule.insert)
def test_daily_prorata_based_depr_on_sl_method(self): 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( asset = create_asset(
calculate_depreciation=1, calculate_depreciation=1,
depreciation_method="Straight Line", depreciation_method="Straight Line",
@@ -178,15 +184,15 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
) )
expected_schedules = [ expected_schedules = [
["2024-12-31", 60.92, 284.07], ["2024-12-31", 60.98, 284.13],
["2025-03-31", 60.92, 344.99], ["2025-03-31", 60.98, 345.11],
["2025-06-30", 60.92, 405.91], ["2025-06-30", 60.98, 406.09],
["2025-09-30", 60.92, 466.83], ["2025-09-30", 60.98, 467.07],
["2025-12-31", 60.92, 527.75], ["2025-12-31", 60.98, 528.05],
["2026-03-31", 60.92, 588.67], ["2026-03-31", 60.98, 589.03],
["2026-06-30", 60.92, 649.59], ["2026-06-30", 60.98, 650.01],
["2026-09-30", 60.92, 710.51], ["2026-09-30", 60.98, 710.99],
["2026-11-01", 20.49, 731.0], ["2026-11-01", 20.01, 731.0],
] ]
schedules = [ schedules = [
[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount] [cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount]
@@ -273,12 +279,12 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
expected_schedules = [ expected_schedules = [
["2021-03-31", 4383.56, 4383.56], ["2021-03-31", 4383.56, 4383.56],
["2021-06-30", 9535.45, 13919.01], ["2021-06-30", 9972.6, 14356.16],
["2021-09-30", 9640.23, 23559.24], ["2021-09-30", 10082.19, 24438.35],
["2021-12-31", 9640.23, 33199.47], ["2021-12-31", 10082.19, 34520.54],
["2022-03-31", 9430.66, 42630.13], ["2022-03-31", 6458.25, 40978.79],
["2022-06-30", 5721.27, 48351.4], ["2022-06-30", 6530.01, 47508.8],
["2022-08-20", 51648.6, 100000.0], ["2022-08-20", 52491.2, 100000.0],
] ]
schedules = [ schedules = [
@@ -302,13 +308,13 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
) )
expected_schedules = [ expected_schedules = [
["2020-02-29", 1092.90, 1092.90], ["2020-02-29", 1092.9, 1092.9],
["2020-08-31", 19944.01, 21036.91], ["2020-08-31", 20109.29, 21202.19],
["2021-02-28", 19618.83, 40655.74], ["2021-02-28", 15630.03, 36832.22],
["2021-08-31", 11966.4, 52622.14], ["2021-08-31", 15889.09, 52721.31],
["2022-02-28", 11771.3, 64393.44], ["2022-02-28", 9378.02, 62099.33],
["2022-08-31", 7179.84, 71573.28], ["2022-08-31", 9533.46, 71632.79],
["2023-02-20", 28426.72, 100000.0], ["2023-02-20", 28367.21, 100000.0],
] ]
schedules = [ schedules = [
@@ -377,36 +383,790 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
self.assertEqual(asset.finance_books[0].total_number_of_booked_depreciations, 14) 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( asset = create_asset(
item_code="Macbook Pro",
gross_purchase_amount=500,
calculate_depreciation=1, calculate_depreciation=1,
depreciation_method="Written Down Value", depreciation_method="Straight Line",
available_for_use_date="2020-07-17", available_for_use_date="2023-01-01",
is_existing_asset=1, depreciation_start_date="2023-01-31",
opening_number_of_booked_depreciations=2, frequency_of_depreciation=1,
opening_accumulated_depreciation=11666.67,
depreciation_start_date="2021-04-30",
total_number_of_depreciations=12, total_number_of_depreciations=12,
frequency_of_depreciation=3, submit=1,
gross_purchase_amount=50000,
rate_of_depreciation=40,
) )
self.assertEqual(asset.status, "Draft") expected_depreciation_before_repair = [
expected_schedules = [ ["2023-01-31", 41.67, 41.67],
["2021-04-30", 3833.33, 15500.0], ["2023-02-28", 41.67, 83.34],
["2021-07-31", 3833.33, 19333.33], ["2023-03-31", 41.67, 125.01],
["2021-10-31", 3833.33, 23166.66], ["2023-04-30", 41.67, 166.68],
["2022-01-31", 3833.33, 26999.99], ["2023-05-31", 41.67, 208.35],
["2022-04-30", 2300.0, 29299.99], ["2023-06-30", 41.67, 250.02],
["2022-07-31", 2300.0, 31599.99], ["2023-07-31", 41.67, 291.69],
["2022-10-31", 2300.0, 33899.99], ["2023-08-31", 41.67, 333.36],
["2023-01-31", 2300.0, 36199.99], ["2023-09-30", 41.67, 375.03],
["2023-04-30", 1380.0, 37579.99], ["2023-10-31", 41.67, 416.7],
["2023-07-31", 12420.01, 50000.0], ["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 = [ schedules = [
[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount] [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in get_depr_schedule(asset.name, "Draft") 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)

View File

@@ -9,6 +9,7 @@
"depreciation_method", "depreciation_method",
"frequency_of_depreciation", "frequency_of_depreciation",
"total_number_of_depreciations", "total_number_of_depreciations",
"increase_in_asset_life",
"depreciation_start_date", "depreciation_start_date",
"column_break_5", "column_break_5",
"salvage_value_percentage", "salvage_value_percentage",
@@ -89,7 +90,8 @@
"fieldname": "rate_of_depreciation", "fieldname": "rate_of_depreciation",
"fieldtype": "Percent", "fieldtype": "Percent",
"label": "Rate of Depreciation (%)", "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", "fieldname": "salvage_value_percentage",
@@ -115,6 +117,7 @@
"fieldname": "total_number_of_booked_depreciations", "fieldname": "total_number_of_booked_depreciations",
"fieldtype": "Int", "fieldtype": "Int",
"label": "Total Number of Booked Depreciations ", "label": "Total Number of Booked Depreciations ",
"no_copy": 1,
"read_only": 1 "read_only": 1
}, },
{ {
@@ -124,12 +127,20 @@
{ {
"fieldname": "column_break_sigk", "fieldname": "column_break_sigk",
"fieldtype": "Column Break" "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, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2024-12-13 12:11:03.743209", "modified": "2025-01-06 17:14:51.836803",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset Finance Book", "name": "Asset Finance Book",

View File

@@ -22,6 +22,7 @@ class AssetFinanceBook(Document):
expected_value_after_useful_life: DF.Currency expected_value_after_useful_life: DF.Currency
finance_book: DF.Link | None finance_book: DF.Link | None
frequency_of_depreciation: DF.Int frequency_of_depreciation: DF.Int
increase_in_asset_life: DF.Int
parent: DF.Data parent: DF.Data
parentfield: DF.Data parentfield: DF.Data
parenttype: DF.Data parenttype: DF.Data

View File

@@ -34,7 +34,16 @@ frappe.ui.form.on("Asset Repair", {
query: "erpnext.assets.doctype.asset_repair.asset_repair.get_purchase_invoice", query: "erpnext.assets.doctype.asset_repair.asset_repair.get_purchase_invoice",
filters: { filters: {
company: frm.doc.company, 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) { refresh: function (frm) {

View File

@@ -7,40 +7,42 @@
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"naming_series",
"asset", "asset",
"asset_name",
"company", "company",
"column_break_2", "column_break_2",
"asset_name",
"naming_series",
"section_break_5",
"failure_date",
"repair_status", "repair_status",
"column_break_6", "failure_date",
"completion_date", "completion_date",
"accounting_dimensions_section", "downtime",
"cost_center", "amended_from",
"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",
"section_break_9", "section_break_9",
"description", "description",
"column_break_9", "column_break_9",
"actions_performed", "actions_performed",
"section_break_23", "accounting_details",
"downtime", "invoices",
"column_break_19", "section_break_muyc",
"amended_from" "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": [ "fields": [
{ {
@@ -54,11 +56,6 @@
"fieldname": "column_break_2", "fieldname": "column_break_2",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{
"fieldname": "section_break_5",
"fieldtype": "Section Break",
"label": "Repair Details"
},
{ {
"columns": 1, "columns": 1,
"fieldname": "failure_date", "fieldname": "failure_date",
@@ -67,11 +64,7 @@
"reqd": 1 "reqd": 1
}, },
{ {
"fieldname": "column_break_6", "depends_on": "eval:doc.repair_status==\"Completed\"",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:!doc.__islocal",
"fieldname": "completion_date", "fieldname": "completion_date",
"fieldtype": "Datetime", "fieldtype": "Datetime",
"label": "Completion Date", "label": "Completion Date",
@@ -79,7 +72,6 @@
}, },
{ {
"default": "Pending", "default": "Pending",
"depends_on": "eval:!doc.__islocal",
"fieldname": "repair_status", "fieldname": "repair_status",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Repair Status", "label": "Repair Status",
@@ -113,10 +105,6 @@
"label": "Downtime", "label": "Downtime",
"read_only": 1 "read_only": 1
}, },
{
"fieldname": "column_break_19",
"fieldtype": "Column Break"
},
{ {
"default": "0", "default": "0",
"fieldname": "repair_cost", "fieldname": "repair_cost",
@@ -148,10 +136,6 @@
"fieldtype": "Read Only", "fieldtype": "Read Only",
"label": "Asset Name" "label": "Asset Name"
}, },
{
"fieldname": "column_break_8",
"fieldtype": "Column Break"
},
{ {
"default": "0", "default": "0",
"depends_on": "eval:!doc.__islocal", "depends_on": "eval:!doc.__islocal",
@@ -162,7 +146,8 @@
{ {
"fieldname": "accounting_details", "fieldname": "accounting_details",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Accounting Details" "hide_border": 1,
"label": "Repair Purchase Invoices"
}, },
{ {
"fieldname": "stock_items", "fieldname": "stock_items",
@@ -172,16 +157,7 @@
"options": "Asset Repair Consumed Item" "options": "Asset Repair Consumed Item"
}, },
{ {
"fieldname": "section_break_23", "fetch_from": "company.cost_center",
"fieldtype": "Section Break"
},
{
"collapsible": 1,
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
"label": "Accounting Dimensions"
},
{
"fieldname": "cost_center", "fieldname": "cost_center",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Cost Center", "label": "Cost Center",
@@ -198,21 +174,13 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"default": "0", "collapsible_depends_on": "stock_items",
"depends_on": "eval:!doc.__islocal",
"fieldname": "stock_consumption",
"fieldtype": "Check",
"label": "Stock Consumed During Repair"
},
{
"depends_on": "stock_consumption",
"fieldname": "stock_consumption_details_section", "fieldname": "stock_consumption_details_section",
"fieldtype": "Section Break", "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", "fieldname": "total_repair_cost",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Total Repair Cost", "label": "Total Repair Cost",
@@ -220,11 +188,6 @@
}, },
{ {
"depends_on": "capitalize_repair_cost", "depends_on": "capitalize_repair_cost",
"fieldname": "asset_depreciation_details_section",
"fieldtype": "Section Break",
"label": "Asset Depreciation Details"
},
{
"fieldname": "increase_in_asset_life", "fieldname": "increase_in_asset_life",
"fieldtype": "Int", "fieldtype": "Int",
"label": "Increase In Asset Life(Months)", "label": "Increase In Asset Life(Months)",
@@ -240,20 +203,63 @@
{ {
"fieldname": "invoices", "fieldname": "invoices",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Asset Repair Purchase Invoices",
"mandatory_depends_on": "eval: doc.repair_status == 'Completed' && doc.repair_cost > 0;", "mandatory_depends_on": "eval: doc.repair_status == 'Completed' && doc.repair_cost > 0;",
"no_copy": 1, "no_copy": 1,
"options": "Asset Repair Purchase Invoice" "options": "Asset Repair Purchase Invoice"
}, },
{ {
"fieldname": "section_break_y7cc", "fieldname": "section_break_muyc",
"fieldtype": "Section Break" "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, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2024-09-30 13:02:06.931188", "modified": "2024-12-27 18:11:40.548727",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset Repair", "name": "Asset Repair",

View File

@@ -4,7 +4,7 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.query_builder import DocType 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 import erpnext
from erpnext.accounts.general_ledger import make_gl_entries 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_activity.asset_activity import add_asset_activity
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_depr_schedule, get_depr_schedule,
make_new_active_asset_depr_schedules_and_cancel_current_ones, reschedule_depreciation,
) )
from erpnext.controllers.accounts_controller import AccountsController from erpnext.controllers.accounts_controller import AccountsController
@@ -40,6 +40,7 @@ class AssetRepair(AccountsController):
capitalize_repair_cost: DF.Check capitalize_repair_cost: DF.Check
company: DF.Link | None company: DF.Link | None
completion_date: DF.Datetime | None completion_date: DF.Datetime | None
consumed_items_cost: DF.Currency
cost_center: DF.Link | None cost_center: DF.Link | None
description: DF.LongText | None description: DF.LongText | None
downtime: DF.Data | None downtime: DF.Data | None
@@ -50,7 +51,6 @@ class AssetRepair(AccountsController):
project: DF.Link | None project: DF.Link | None
repair_cost: DF.Currency repair_cost: DF.Currency
repair_status: DF.Literal["Pending", "Completed", "Cancelled"] repair_status: DF.Literal["Pending", "Completed", "Cancelled"]
stock_consumption: DF.Check
stock_items: DF.Table[AssetRepairConsumedItem] stock_items: DF.Table[AssetRepairConsumedItem]
total_repair_cost: DF.Currency total_repair_cost: DF.Currency
# end: auto-generated types # end: auto-generated types
@@ -58,16 +58,12 @@ class AssetRepair(AccountsController):
def validate(self): def validate(self):
self.asset_doc = frappe.get_doc("Asset", self.asset) self.asset_doc = frappe.get_doc("Asset", self.asset)
self.validate_dates() self.validate_dates()
self.validate_purchase_invoice() self.validate_purchase_invoices()
self.validate_purchase_invoice_repair_cost()
self.validate_purchase_invoice_expense_account()
self.update_status() self.update_status()
self.calculate_consumed_items_cost()
if self.get("stock_items"):
self.set_stock_items_cost()
self.calculate_repair_cost() self.calculate_repair_cost()
self.calculate_total_repair_cost() self.calculate_total_repair_cost()
self.check_repair_status()
def validate_dates(self): def validate_dates(self):
if self.completion_date and (self.failure_date > self.completion_date): 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.") _("Completion Date can not be before Failure Date. Please adjust the dates accordingly.")
) )
def validate_purchase_invoice(self): def validate_purchase_invoices(self):
query = expense_item_pi_query(self.company) for d in self.invoices:
purchase_invoice_list = [item[0] for item in query.run()] invoice_items = self.get_invoice_items(d.purchase_invoice)
for pi in self.invoices: self.validate_service_purchase_invoice(d.purchase_invoice, invoice_items)
if pi.purchase_invoice not in purchase_invoice_list: self.validate_expense_account(d, invoice_items)
frappe.throw(_("Expense item not present in Purchase Invoice")) self.validate_purchase_invoice_repair_cost(d, invoice_items)
def validate_purchase_invoice_repair_cost(self): def get_invoice_items(self, pi):
for pi in self.invoices: invoice_items = frappe.get_all(
if flt(pi.repair_cost) > frappe.db.get_value( "Purchase Invoice Item",
"Purchase Invoice", pi.purchase_invoice, "base_net_total" filters={"parent": pi},
): fields=["item_code", "expense_account", "base_net_amount"],
frappe.throw(_("Repair cost cannot be greater than purchase invoice base net total")) )
def validate_purchase_invoice_expense_account(self): return invoice_items
for pi in self.invoices:
if pi.expense_account not in frappe.db.get_all( def validate_service_purchase_invoice(self, purchase_invoice, invoice_items):
"Purchase Invoice Item", {"parent": pi.purchase_invoice}, pluck="expense_account" service_item_exists = False
): for item in invoice_items:
frappe.throw( if frappe.db.get_value("Item", item.item_code, "is_stock_item") == 0:
_("Expense account not present in Purchase Invoice {0}").format( service_item_exists = True
get_link_to_form("Purchase Invoice", pi.purchase_invoice) 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): def update_status(self):
if self.repair_status == "Pending" and self.asset_doc.status != "Out of Order": if self.repair_status == "Pending" and self.asset_doc.status != "Out of Order":
frappe.db.set_value("Asset", self.asset, "status", "Out of Order") frappe.db.set_value("Asset", self.asset, "status", "Out of Order")
add_asset_activity( self.add_asset_activity(
self.asset,
_("Asset out of order due to Asset Repair {0}").format( _("Asset out of order due to Asset Repair {0}").format(
get_link_to_form("Asset Repair", self.name) get_link_to_form("Asset Repair", self.name)
), ),
@@ -112,147 +130,78 @@ class AssetRepair(AccountsController):
else: else:
self.asset_doc.set_status() 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"): for item in self.get("stock_items"):
item.total_value = flt(item.valuation_rate) * flt(item.consumed_quantity) 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): def calculate_repair_cost(self):
self.repair_cost = sum(flt(pi.repair_cost) for pi in self.invoices) self.repair_cost = sum(flt(pi.repair_cost) for pi in self.invoices)
def calculate_total_repair_cost(self): 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() def on_submit(self):
self.total_repair_cost += total_value_of_stock_consumed self.decrease_stock_quantity()
def before_submit(self): if self.get("capitalize_repair_cost"):
self.check_repair_status() 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.make_gl_entries()
self.asset_doc.flags.increase_in_asset_value_due_to_repair = True
self.increase_asset_value() def on_cancel(self):
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):
self.asset_doc = frappe.get_doc("Asset", self.asset) 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 depreciation_note = self.get_depreciation_note()
reschedule_depreciation(self.asset_doc, depreciation_note)
if self.get("stock_consumption") or self.get("capitalize_repair_cost"): self.add_asset_activity()
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)
),
)
def after_delete(self): def after_delete(self):
frappe.get_doc("Asset", self.asset).set_status() frappe.get_doc("Asset", self.asset).set_status()
def check_repair_status(self): 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.")) frappe.throw(_("Please update Repair Status."))
def check_for_stock_items_and_warehouse(self): def update_asset_value(self):
if not self.get("stock_items"): total_repair_cost = self.total_repair_cost if self.docstatus == 1 else -1 * self.total_repair_cost
frappe.throw(_("Please enter Stock Items consumed during the Repair."), title=_("Missing Items"))
def increase_asset_value(self): self.asset_doc.total_asset_cost += flt(total_repair_cost)
total_value_of_stock_consumed = self.get_total_value_of_stock_consumed() self.asset_doc.additional_asset_cost += flt(total_repair_cost)
if self.asset_doc.calculate_depreciation: if self.asset_doc.calculate_depreciation:
for row in self.asset_doc.finance_books: 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: self.asset_doc.flags.ignore_validate_update_after_submit = True
row.value_after_depreciation += self.repair_cost self.asset_doc.save()
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
def get_total_value_of_stock_consumed(self): def get_total_value_of_stock_consumed(self):
total_value_of_stock_consumed = 0 return sum([flt(item.total_value) for item in self.get("stock_items")])
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
def decrease_stock_quantity(self): def decrease_stock_quantity(self):
if not self.get("stock_items"):
return
stock_entry = frappe.get_doc( 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"): for stock_item in self.get("stock_items"):
self.validate_serial_no(stock_item) self.validate_serial_no(stock_item)
@@ -278,7 +227,7 @@ class AssetRepair(AccountsController):
"Item", stock_item.item_code, "has_serial_no" "Item", stock_item.item_code, "has_serial_no"
): ):
msg = f"Serial No Bundle is mandatory for Item {stock_item.item_code}" 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: if stock_item.serial_and_batch_bundle:
values_to_update = { values_to_update = {
@@ -291,6 +240,9 @@ class AssetRepair(AccountsController):
) )
def make_gl_entries(self, cancel=False): 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: if flt(self.total_repair_cost) > 0:
gl_entries = self.get_gl_entries() gl_entries = self.get_gl_entries()
make_gl_entries(gl_entries, cancel) 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): 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 return
# creating GL Entries for each row in Stock Items based on the Stock Entry created for it # 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): def set_increase_in_asset_life(self):
for row in self.asset_doc.finance_books: if self.asset_doc.calculate_depreciation and cint(self.increase_in_asset_life) > 0:
row.total_number_of_depreciations += self.increase_in_asset_life / row.frequency_of_depreciation 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 def get_depreciation_note(self):
extra_months = self.increase_in_asset_life % row.frequency_of_depreciation return _("This schedule was created when Asset {0} was repaired through Asset Repair {1}.").format(
if extra_months != 0: get_link_to_form(self.asset_doc.doctype, self.asset_doc.name),
self.calculate_last_schedule_date(self.asset_doc, row, extra_months) get_link_to_form(self.doctype, self.name),
# 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
) )
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 add_asset_activity(self.asset, subject)
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
@frappe.whitelist() @frappe.whitelist()
@@ -476,16 +384,11 @@ def get_downtime(failure_date, completion_date):
@frappe.whitelist() @frappe.whitelist()
def get_purchase_invoice(doctype, txt, searchfield, start, page_len, filters): 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") PurchaseInvoice = DocType("Purchase Invoice")
PurchaseInvoiceItem = DocType("Purchase Invoice Item") PurchaseInvoiceItem = DocType("Purchase Invoice Item")
Item = DocType("Item") Item = DocType("Item")
query = ( return (
frappe.qb.from_(PurchaseInvoice) frappe.qb.from_(PurchaseInvoice)
.join(PurchaseInvoiceItem) .join(PurchaseInvoiceItem)
.on(PurchaseInvoiceItem.parent == PurchaseInvoice.name) .on(PurchaseInvoiceItem.parent == PurchaseInvoice.name)
@@ -495,8 +398,18 @@ def expense_item_pi_query(company):
.where( .where(
(Item.is_stock_item == 0) (Item.is_stock_item == 0)
& (Item.is_fixed_asset == 0) & (Item.is_fixed_asset == 0)
& (PurchaseInvoice.company == company) & (PurchaseInvoice.company == filters.get("company"))
& (PurchaseInvoice.docstatus == 1) & (PurchaseInvoice.docstatus == 1)
) )
) ).run(as_list=1)
return query
@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)

View File

@@ -128,7 +128,11 @@ class TestAssetRepair(IntegrationTestCase):
asset = create_asset(calculate_depreciation=1, submit=1) asset = create_asset(calculate_depreciation=1, submit=1)
initial_asset_value = get_asset_value_after_depreciation(asset.name) initial_asset_value = get_asset_value_after_depreciation(asset.name)
asset_repair = create_asset_repair( 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() asset.reload()
@@ -136,7 +140,9 @@ class TestAssetRepair(IntegrationTestCase):
self.assertEqual(asset_repair.repair_cost, increase_in_asset_value) self.assertEqual(asset_repair.repair_cost, increase_in_asset_value)
def test_purchase_invoice(self): 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) self.assertTrue(asset_repair.invoices)
def test_gl_entries_with_perpetual_inventory(self): def test_gl_entries_with_perpetual_inventory(self):
@@ -163,6 +169,7 @@ class TestAssetRepair(IntegrationTestCase):
pi_expense_account1="Administrative Expenses - TCP1", pi_expense_account1="Administrative Expenses - TCP1",
pi_expense_account2="Legal Expenses - TCP1", pi_expense_account2="Legal Expenses - TCP1",
item="_Test Non Stock Item", item="_Test Non Stock Item",
increase_in_asset_life=1,
submit=1, submit=1,
) )
@@ -210,6 +217,7 @@ class TestAssetRepair(IntegrationTestCase):
asset_repair = create_asset_repair( asset_repair = create_asset_repair(
capitalize_repair_cost=1, capitalize_repair_cost=1,
stock_consumption=1, stock_consumption=1,
increase_in_asset_life=1,
item="_Test Non Stock Item", item="_Test Non Stock Item",
submit=1, submit=1,
) )
@@ -259,7 +267,13 @@ class TestAssetRepair(IntegrationTestCase):
self.assertEqual(first_asset_depr_schedule.status, "Active") self.assertEqual(first_asset_depr_schedule.status, "Active")
initial_num_of_depreciations = num_of_depreciations(asset) 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() asset.reload()
first_asset_depr_schedule.load_from_db() first_asset_depr_schedule.load_from_db()
@@ -282,7 +296,9 @@ class TestAssetRepair(IntegrationTestCase):
def num_of_depreciations(asset): 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): def create_asset_repair(**args):
@@ -300,7 +316,7 @@ def create_asset_repair(**args):
{ {
"asset": asset.name, "asset": asset.name,
"asset_name": asset.asset_name, "asset_name": asset.asset_name,
"failure_date": nowdate(), "failure_date": args.failure_date or nowdate(),
"description": "Test Description", "description": "Test Description",
"company": asset.company, "company": asset.company,
} }
@@ -364,7 +380,7 @@ def create_asset_repair(**args):
if args.capitalize_repair_cost: if args.capitalize_repair_cost:
asset_repair.capitalize_repair_cost = 1 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 asset_repair.increase_in_asset_life = 12
pi1 = make_purchase_invoice( pi1 = make_purchase_invoice(
company=asset.company, company=asset.company,

View File

@@ -2,6 +2,15 @@
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on("Asset Shift Allocation", { frappe.ui.form.on("Asset Shift Allocation", {
onload: function (frm) { onload: function (frm) {
frm.set_query("asset", function () {
return {
filters: {
company: frm.doc.company,
docstatus: 1,
},
};
});
frm.events.make_schedules_editable(frm); frm.events.make_schedules_editable(frm);
}, },

View File

@@ -12,6 +12,7 @@
"finance_book", "finance_book",
"amended_from", "amended_from",
"depreciation_schedule_section", "depreciation_schedule_section",
"column_break_jomc",
"depreciation_schedule" "depreciation_schedule"
], ],
"fields": [ "fields": [
@@ -57,7 +58,9 @@
"fieldname": "depreciation_schedule", "fieldname": "depreciation_schedule",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Depreciation Schedule", "label": "Depreciation Schedule",
"options": "Depreciation Schedule" "no_copy": 1,
"options": "Depreciation Schedule",
"read_only": 1
}, },
{ {
"fieldname": "naming_series", "fieldname": "naming_series",
@@ -65,12 +68,16 @@
"label": "Naming Series", "label": "Naming Series",
"options": "ACC-ASA-.YYYY.-", "options": "ACC-ASA-.YYYY.-",
"reqd": 1 "reqd": 1
},
{
"fieldname": "column_break_jomc",
"fieldtype": "Column Break"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2024-03-27 13:06:35.732191", "modified": "2025-01-10 16:25:31.397325",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset Shift Allocation", "name": "Asset Shift Allocation",

View File

@@ -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 ( from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_asset_depr_schedule_doc, get_asset_depr_schedule_doc,
get_asset_shift_factors_map, 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: if TYPE_CHECKING:
from frappe.types import DF from frappe.types import DF
from erpnext.assets.doctype.depreciation_schedule.depreciation_schedule import ( from erpnext.assets.doctype.depreciation_schedule.depreciation_schedule import DepreciationSchedule
DepreciationSchedule,
)
amended_from: DF.Link | None amended_from: DF.Link | None
asset: DF.Link asset: DF.Link
@@ -41,32 +39,138 @@ class AssetShiftAllocation(Document):
naming_series: DF.Literal["ACC-ASA-.YYYY.-"] naming_series: DF.Literal["ACC-ASA-.YYYY.-"]
# end: auto-generated types # end: auto-generated types
def after_insert(self):
self.fetch_and_set_depr_schedule()
def validate(self): def validate(self):
self.asset_depr_schedule_doc = get_asset_depr_schedule_doc(self.asset, "Active", self.finance_book) 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() def after_insert(self):
self.update_depr_schedule() self.fetch_and_set_depr_schedule()
def on_submit(self): def on_submit(self):
self.create_new_asset_depr_schedule() 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): def fetch_and_set_depr_schedule(self):
if self.asset_depr_schedule_doc: if self.asset_depr_schedule_doc:
if self.asset_depr_schedule_doc.shift_based: if self.asset_depr_schedule_doc.shift_based:
for schedule in self.asset_depr_schedule_doc.get("depreciation_schedule"): self.modify_depr_schedule(self.asset_depr_schedule_doc.depreciation_schedule)
self.append(
"depreciation_schedule",
{
"schedule_date": schedule.schedule_date,
"depreciation_amount": schedule.depreciation_amount,
"accumulated_depreciation_amount": schedule.accumulated_depreciation_amount,
"journal_entry": schedule.journal_entry,
"shift": schedule.shift,
},
)
self.flags.ignore_validate = True self.flags.ignore_validate = True
self.save() 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): def create_new_asset_depr_schedule(self):
new_asset_depr_schedule_doc = frappe.copy_doc(self.asset_depr_schedule_doc) new_asset_depr_schedule_doc = frappe.copy_doc(self.asset_depr_schedule_doc)
@@ -257,17 +224,3 @@ class AssetShiftAllocation(Document):
get_link_to_form(self.doctype, self.name) 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)

View File

@@ -45,6 +45,9 @@ frappe.ui.form.on("Asset Value Adjustment", {
asset: function (frm) { asset: function (frm) {
frm.trigger("set_acc_dimension"); frm.trigger("set_acc_dimension");
if (frm.doc.asset) {
frm.trigger("set_current_asset_value");
}
}, },
finance_book: function (frm) { finance_book: function (frm) {

View File

@@ -54,8 +54,8 @@
"fieldname": "journal_entry", "fieldname": "journal_entry",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Journal Entry", "label": "Journal Entry",
"options": "Journal Entry",
"no_copy": 1, "no_copy": 1,
"options": "Journal Entry",
"read_only": 1 "read_only": 1
}, },
{ {
@@ -125,18 +125,18 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"fieldname": "difference_account", "fieldname": "difference_account",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Difference Account", "label": "Difference Account",
"no_copy": 1, "no_copy": 1,
"options": "Account", "options": "Account",
"reqd": 1 "reqd": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2024-08-13 16:21:18.639208", "modified": "2024-12-18 15:04:18.726505",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset Value Adjustment", "name": "Asset Value Adjustment",
@@ -188,7 +188,6 @@
"write": 1 "write": 1
} }
], ],
"quick_entry": 1,
"sort_field": "creation", "sort_field": "creation",
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],

View File

@@ -5,7 +5,7 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document 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 ( from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_checks_for_pl_and_bs_accounts, 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.depreciation import get_depreciation_accounts
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( 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_current_asset_value()
self.set_difference_amount() 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 <b>{0}</b>.").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): def on_submit(self):
self.make_depreciation_entry() self.make_asset_revaluation_entry()
self.set_value_after_depreciation() self.update_asset()
self.update_asset(self.new_asset_value)
add_asset_activity( add_asset_activity(
self.asset, self.asset,
_("Asset's value adjusted after submission of Asset Value Adjustment {0}").format( _("Asset's value adjusted after submission of Asset Value Adjustment {0}").format(
@@ -67,27 +83,7 @@ class AssetValueAdjustment(Document):
), ),
) )
def validate_date(self): def make_asset_revaluation_entry(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 <b>{0}</b>.").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):
asset = frappe.get_doc("Asset", self.asset) asset = frappe.get_doc("Asset", self.asset)
( (
fixed_asset_account, fixed_asset_account,
@@ -114,46 +110,15 @@ class AssetValueAdjustment(Document):
} }
if self.difference_amount < 0: if self.difference_amount < 0:
credit_entry = { credit_entry, debit_entry = self.get_entry_for_asset_value_decrease(
"account": fixed_asset_account, fixed_asset_account, entry_template
"credit_in_account_currency": -self.difference_amount, )
**entry_template,
}
debit_entry = {
"account": self.difference_account,
"debit_in_account_currency": -self.difference_amount,
**entry_template,
}
elif self.difference_amount > 0: elif self.difference_amount > 0:
credit_entry = { credit_entry, debit_entry = self.get_entry_for_asset_value_increase(
"account": self.difference_account, fixed_asset_account, entry_template
"credit_in_account_currency": self.difference_amount, )
**entry_template,
}
debit_entry = {
"account": fixed_asset_account,
"debit_in_account_currency": self.difference_amount,
**entry_template,
}
accounting_dimensions = get_checks_for_pl_and_bs_accounts() self.update_accounting_dimensions(credit_entry, debit_entry)
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")
}
)
je.append("accounts", credit_entry) je.append("accounts", credit_entry)
je.append("accounts", debit_entry) je.append("accounts", debit_entry)
@@ -163,40 +128,81 @@ class AssetValueAdjustment(Document):
self.db_set("journal_entry", je.name) 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) 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 += flt(difference_amount)
asset.value_after_depreciation = asset_value asset.db_update()
asset.save() return asset
return
asset.flags.decrease_in_asset_value_due_to_value_adjustment = True
def get_adjustment_note(self):
if self.docstatus == 1: if self.docstatus == 1:
notes = _( notes = _(
"This schedule was created when Asset {0} was adjusted through Asset Value Adjustment {1}." "This schedule was created when Asset {0} was adjusted through Asset Value Adjustment {1}."
).format( ).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")), get_link_to_form(self.get("doctype"), self.get("name")),
) )
elif self.docstatus == 2: elif self.docstatus == 2:
notes = _( notes = _(
"This schedule was created when Asset {0}'s Asset Value Adjustment {1} was cancelled." "This schedule was created when Asset {0}'s Asset Value Adjustment {1} was cancelled."
).format( ).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")), get_link_to_form(self.get("doctype"), self.get("name")),
) )
make_new_active_asset_depr_schedules_and_cancel_current_ones( return notes
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()
@frappe.whitelist() @frappe.whitelist()

View File

@@ -114,12 +114,12 @@ class TestAssetValueAdjustment(IntegrationTestCase):
["2023-05-31", 9983.33, 45408.05], ["2023-05-31", 9983.33, 45408.05],
["2023-06-30", 9983.33, 55391.38], ["2023-06-30", 9983.33, 55391.38],
["2023-07-31", 9983.33, 65374.71], ["2023-07-31", 9983.33, 65374.71],
["2023-08-31", 8300.0, 73674.71], ["2023-08-31", 9070.36, 74445.07],
["2023-09-30", 8300.0, 81974.71], ["2023-09-30", 9070.36, 83515.43],
["2023-10-31", 8300.0, 90274.71], ["2023-10-31", 9070.36, 92585.79],
["2023-11-30", 8300.0, 98574.71], ["2023-11-30", 9070.36, 101656.15],
["2023-12-31", 8300.0, 106874.71], ["2023-12-31", 9070.36, 110726.51],
["2024-01-15", 8300.0, 115174.71], ["2024-01-15", 4448.2, 115174.71],
] ]
schedules = [ schedules = [
@@ -154,7 +154,11 @@ class TestAssetValueAdjustment(IntegrationTestCase):
# create asset repair # create asset repair
asset_repair = 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") 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-05-31", 9983.33, 45408.05],
["2023-06-30", 9983.33, 55391.38], ["2023-06-30", 9983.33, 55391.38],
["2023-07-31", 9983.33, 65374.71], ["2023-07-31", 9983.33, 65374.71],
["2023-08-31", 2766.67, 68141.38], ["2023-08-31", 2847.27, 68221.98],
["2023-09-30", 2766.67, 70908.05], ["2023-09-30", 2847.27, 71069.25],
["2023-10-31", 2766.67, 73674.72], ["2023-10-31", 2847.27, 73916.52],
["2023-11-30", 2766.67, 76441.39], ["2023-11-30", 2847.27, 76763.79],
["2023-12-31", 2766.67, 79208.06], ["2023-12-31", 2847.27, 79611.06],
["2024-01-31", 2766.67, 81974.73], ["2024-01-31", 2847.27, 82458.33],
["2024-02-29", 2766.67, 84741.4], ["2024-02-29", 2847.27, 85305.6],
["2024-03-31", 2766.67, 87508.07], ["2024-03-31", 2847.27, 88152.87],
["2024-04-30", 2766.67, 90274.74], ["2024-04-30", 2847.27, 91000.14],
["2024-05-31", 2766.67, 93041.41], ["2024-05-31", 2847.27, 93847.41],
["2024-06-30", 2766.67, 95808.08], ["2024-06-30", 2847.27, 96694.68],
["2024-07-31", 2766.67, 98574.75], ["2024-07-31", 2847.27, 99541.95],
["2024-08-31", 2766.67, 101341.42], ["2024-08-31", 2847.27, 102389.22],
["2024-09-30", 2766.67, 104108.09], ["2024-09-30", 2847.27, 105236.49],
["2024-10-31", 2766.67, 106874.76], ["2024-10-31", 2847.27, 108083.76],
["2024-11-30", 2766.67, 109641.43], ["2024-11-30", 2847.27, 110931.03],
["2024-12-31", 2766.67, 112408.1], ["2024-12-31", 2847.27, 113778.3],
["2025-01-15", 2766.61, 115174.71], ["2025-01-31", 1396.41, 115174.71],
] ]
schedules = [ schedules = [
@@ -246,12 +250,12 @@ class TestAssetValueAdjustment(IntegrationTestCase):
["2023-05-31", 9983.33, 45408.05], ["2023-05-31", 9983.33, 45408.05],
["2023-06-30", 9983.33, 55391.38], ["2023-06-30", 9983.33, 55391.38],
["2023-07-31", 9983.33, 65374.71], ["2023-07-31", 9983.33, 65374.71],
["2023-08-31", 8208.33, 73583.04], ["2023-08-31", 8970.18, 74344.89],
["2023-09-30", 8208.33, 81791.37], ["2023-09-30", 8970.18, 83315.07],
["2023-10-31", 8208.33, 89999.7], ["2023-10-31", 8970.18, 92285.25],
["2023-11-30", 8208.33, 98208.03], ["2023-11-30", 8970.18, 101255.43],
["2023-12-31", 8208.33, 106416.36], ["2023-12-31", 8970.18, 110225.61],
["2024-01-15", 8208.35, 114624.71], ["2024-01-15", 4399.1, 114624.71],
] ]
schedules = [ schedules = [
@@ -262,7 +266,7 @@ class TestAssetValueAdjustment(IntegrationTestCase):
self.assertEqual(schedules, expected_schedules) self.assertEqual(schedules, expected_schedules)
def test_difference_amount(self): 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_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name")
asset_doc = frappe.get_doc("Asset", asset_name) asset_doc = frappe.get_doc("Asset", asset_name)
@@ -282,17 +286,18 @@ class TestAssetValueAdjustment(IntegrationTestCase):
) )
asset_doc.submit() asset_doc.submit()
current_asset_value = get_asset_value_after_depreciation(asset_doc.name)
adj_doc = make_asset_value_adjustment( adj_doc = make_asset_value_adjustment(
asset=asset_doc.name, asset=asset_doc.name,
current_asset_value=54000, current_asset_value=current_asset_value,
new_asset_value=50000.0, new_asset_value=40000,
date="2023-08-21", date="2023-08-21",
) )
adj_doc.submit() adj_doc.submit()
difference_amount = adj_doc.new_asset_value - adj_doc.current_asset_value 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() 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): def make_asset_value_adjustment(**args):

View File

@@ -411,4 +411,5 @@ erpnext.patches.v15_0.update_payment_schedule_fields_in_invoices
erpnext.patches.v15_0.rename_group_by_to_categorize_by erpnext.patches.v15_0.rename_group_by_to_categorize_by
execute:frappe.db.set_single_value("Accounts Settings", "receivable_payable_fetch_method", "Buffered Cursor") 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.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 erpnext.patches.v15_0.set_grand_total_to_default_mop

View File

@@ -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")