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",
"oldfieldname": "voucher_type",
"oldfieldtype": "Select",
"options": "Journal Entry\nInter Company Journal Entry\nBank Entry\nCash Entry\nCredit Card Entry\nDebit Note\nCredit Note\nContra Entry\nExcise Entry\nWrite Off Entry\nOpening Entry\nDepreciation Entry\nExchange Rate Revaluation\nExchange Gain Or Loss\nDeferred Revenue\nDeferred Expense",
"options": "Journal Entry\nInter Company Journal Entry\nBank Entry\nCash Entry\nCredit Card Entry\nDebit Note\nCredit Note\nContra Entry\nExcise Entry\nWrite Off Entry\nOpening Entry\nDepreciation Entry\nAsset Disposal\nExchange Rate Revaluation\nExchange Gain Or Loss\nDeferred Revenue\nDeferred Expense",
"reqd": 1,
"search_index": 1
},
@@ -557,7 +557,7 @@
"table_fieldname": "payment_entries"
}
],
"modified": "2024-07-18 15:32:29.413598",
"modified": "2024-12-26 15:32:20.730666",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry",

View File

@@ -100,6 +100,7 @@ class JournalEntry(AccountsController):
"Write Off Entry",
"Opening Entry",
"Depreciation Entry",
"Asset Disposal",
"Exchange Rate Revaluation",
"Exchange Gain Or Loss",
"Deferred Revenue",
@@ -140,7 +141,7 @@ class JournalEntry(AccountsController):
self.validate_credit_debit_note()
self.validate_empty_accounts_table()
self.validate_inter_company_accounts()
self.validate_depr_entry_voucher_type()
self.validate_depr_account_and_depr_entry_voucher_type()
self.validate_company_in_accounting_dimension()
self.validate_advance_accounts()
@@ -196,7 +197,6 @@ class JournalEntry(AccountsController):
self.update_asset_value()
self.update_inter_company_jv()
self.update_invoice_discounting()
self.update_booked_depreciation()
def on_update_after_submit(self):
# Flag will be set on Reconciliation
@@ -232,7 +232,6 @@ class JournalEntry(AccountsController):
self.unlink_inter_company_jv()
self.unlink_asset_adjustment_entry()
self.update_invoice_discounting()
self.update_booked_depreciation(1)
def get_title(self):
return self.pay_to_recd_from or self.accounts[0].account
@@ -269,12 +268,16 @@ class JournalEntry(AccountsController):
):
frappe.throw(_("Total Credit/ Debit Amount should be same as linked Journal Entry"))
def validate_depr_entry_voucher_type(self):
if (
any(d.account_type == "Depreciation" for d in self.get("accounts"))
and self.voucher_type != "Depreciation Entry"
):
frappe.throw(_("Journal Entry type should be set as Depreciation Entry for asset depreciation"))
def validate_depr_account_and_depr_entry_voucher_type(self):
for d in self.get("accounts"):
if d.account_type == "Depreciation":
if self.voucher_type != "Depreciation Entry":
frappe.throw(
_("Journal Entry type should be set as Depreciation Entry for asset depreciation")
)
if frappe.get_cached_value("Account", d.account, "root_type") != "Expense":
frappe.throw(_("Account {0} should be of type Expense").format(d.account))
def validate_stock_accounts(self):
stock_accounts = get_stock_accounts(self.company, accounts=self.accounts)
@@ -379,7 +382,11 @@ class JournalEntry(AccountsController):
self.remove(d)
def update_asset_value(self):
if self.flags.planned_depr_entry or self.voucher_type != "Depreciation Entry":
self.update_asset_on_depreciation()
self.update_asset_on_disposal()
def update_asset_on_depreciation(self):
if self.voucher_type != "Depreciation Entry":
return
for d in self.get("accounts"):
@@ -389,22 +396,59 @@ class JournalEntry(AccountsController):
and d.account_type == "Depreciation"
and d.debit
):
asset = frappe.get_doc("Asset", d.reference_name)
asset = frappe.get_cached_doc("Asset", d.reference_name)
if asset.calculate_depreciation:
fb_idx = 1
if self.finance_book:
for fb_row in asset.get("finance_books"):
if fb_row.finance_book == self.finance_book:
fb_idx = fb_row.idx
break
fb_row = asset.get("finance_books")[fb_idx - 1]
fb_row.value_after_depreciation -= d.debit
fb_row.db_update()
else:
asset.db_set("value_after_depreciation", asset.value_after_depreciation - d.debit)
self.update_journal_entry_link_on_depr_schedule(asset, d)
self.update_value_after_depreciation(asset, d.debit)
asset.db_set("value_after_depreciation", asset.value_after_depreciation - d.debit)
asset.set_status()
asset.set_total_booked_depreciations()
def update_value_after_depreciation(self, asset, depr_amount):
fb_idx = 1
if self.finance_book:
for fb_row in asset.get("finance_books"):
if fb_row.finance_book == self.finance_book:
fb_idx = fb_row.idx
break
fb_row = asset.get("finance_books")[fb_idx - 1]
fb_row.value_after_depreciation -= depr_amount
frappe.db.set_value(
"Asset Finance Book", fb_row.name, "value_after_depreciation", fb_row.value_after_depreciation
)
def update_journal_entry_link_on_depr_schedule(self, asset, je_row):
depr_schedule = get_depr_schedule(asset.name, "Active", self.finance_book)
for d in depr_schedule or []:
if (
d.schedule_date == self.posting_date
and not d.journal_entry
and d.depreciation_amount == flt(je_row.debit)
):
frappe.db.set_value("Depreciation Schedule", d.name, "journal_entry", self.name)
def update_asset_on_disposal(self):
if self.voucher_type == "Asset Disposal":
disposed_assets = []
for d in self.get("accounts"):
if (
d.reference_type == "Asset"
and d.reference_name
and d.reference_name not in disposed_assets
):
frappe.db.set_value(
"Asset",
d.reference_name,
{
"disposal_date": self.posting_date,
"journal_entry_for_scrap": self.name,
},
)
asset_doc = frappe.get_doc("Asset", d.reference_name)
asset_doc.set_status()
disposed_assets.append(d.reference_name)
def update_inter_company_jv(self):
if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference:
@@ -459,25 +503,6 @@ class JournalEntry(AccountsController):
if status:
inv_disc_doc.set_status(status=status)
def update_booked_depreciation(self, cancel=0):
for d in self.get("accounts"):
if (
self.voucher_type == "Depreciation Entry"
and d.reference_type == "Asset"
and d.reference_name
and frappe.get_cached_value("Account", d.account, "root_type") == "Expense"
and d.debit
):
asset = frappe.get_doc("Asset", d.reference_name)
for fb_row in asset.get("finance_books"):
if fb_row.finance_book == self.finance_book:
if cancel:
fb_row.total_number_of_booked_depreciations -= 1
else:
fb_row.total_number_of_booked_depreciations += 1
fb_row.db_update()
break
def unlink_advance_entry_reference(self):
for d in self.get("accounts"):
if d.is_advance == "Yes" and d.reference_type in ("Sales Invoice", "Purchase Invoice"):
@@ -527,9 +552,9 @@ class JournalEntry(AccountsController):
fb_row = asset.get("finance_books")[fb_idx - 1]
fb_row.value_after_depreciation += d.debit
fb_row.db_update()
else:
asset.db_set("value_after_depreciation", asset.value_after_depreciation + d.debit)
asset.db_set("value_after_depreciation", asset.value_after_depreciation + d.debit)
asset.set_status()
asset.set_total_booked_depreciations()
elif self.voucher_type == "Journal Entry" and d.reference_type == "Asset" and d.reference_name:
journal_entry_for_scrap = frappe.db.get_value(
"Asset", d.reference_name, "journal_entry_for_scrap"

View File

@@ -39,7 +39,7 @@ from erpnext.assets.doctype.asset.depreciation import (
get_gl_entries_on_asset_disposal,
get_gl_entries_on_asset_regain,
reset_depreciation_schedule,
reverse_depreciation_entry_made_after_disposal,
reverse_depreciation_entry_made_on_disposal,
)
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
from erpnext.controllers.accounts_controller import validate_account_head
@@ -368,21 +368,34 @@ class SalesInvoice(SellingController):
validate_docs_for_deferred_accounting([self.name], [])
def validate_fixed_asset(self):
for d in self.get("items"):
if d.is_fixed_asset and d.meta.get_field("asset") and d.asset:
asset = frappe.get_doc("Asset", d.asset)
if self.doctype == "Sales Invoice" and self.docstatus == 1:
if self.update_stock:
frappe.throw(_("'Update Stock' cannot be checked for fixed asset sale"))
if self.doctype != "Sales Invoice":
return
elif asset.status in ("Scrapped", "Cancelled", "Capitalized") or (
asset.status == "Sold" and not self.is_return
):
frappe.throw(
_("Row #{0}: Asset {1} cannot be submitted, it is already {2}").format(
d.idx, d.asset, asset.status
for d in self.get("items"):
if d.is_fixed_asset:
if d.asset:
if not self.is_return:
asset_status = frappe.db.get_value("Asset", d.asset, "status")
if self.update_stock:
frappe.throw(_("'Update Stock' cannot be checked for fixed asset sale"))
elif asset_status in ("Scrapped", "Cancelled", "Capitalized"):
frappe.throw(
_("Row #{0}: Asset {1} cannot be sold, it is already {2}").format(
d.idx, d.asset, asset_status
)
)
elif asset_status == "Sold" and not self.is_return:
frappe.throw(_("Row #{0}: Asset {1} is already sold").format(d.idx, d.asset))
elif not self.return_against:
frappe.throw(
_("Row #{0}: Return Against is required for returning asset").format(d.idx)
)
else:
frappe.throw(
_("Row #{0}: You must select an Asset for Item {1}.").format(d.idx, d.item_code),
title=_("Missing Asset"),
)
def validate_item_cost_centers(self):
for item in self.items:
@@ -464,6 +477,8 @@ class SalesInvoice(SellingController):
self.update_stock_reservation_entries()
self.update_stock_ledger()
self.process_asset_depreciation()
# this sequence because outstanding may get -ve
self.make_gl_entries()
@@ -583,6 +598,8 @@ class SalesInvoice(SellingController):
if self.update_stock == 1:
self.update_stock_ledger()
self.process_asset_depreciation()
self.make_gl_entries_on_cancel()
if self.update_stock == 1:
@@ -1253,6 +1270,90 @@ class SalesInvoice(SellingController):
):
throw(_("Delivery Note {0} is not submitted").format(d.delivery_note))
def process_asset_depreciation(self):
if (self.is_return and self.docstatus == 2) or (not self.is_return and self.docstatus == 1):
self.depreciate_asset_on_sale()
else:
self.restore_asset()
self.update_asset()
def depreciate_asset_on_sale(self):
"""
Depreciate asset on sale or cancellation of return sales invoice
"""
disposal_date = self.get_disposal_date()
for d in self.get("items"):
if d.asset:
asset = frappe.get_doc("Asset", d.asset)
if asset.calculate_depreciation and asset.status != "Fully Depreciated":
depreciate_asset(asset, disposal_date, self.get_note_for_asset_sale(asset))
def get_note_for_asset_sale(self, asset):
return _("This schedule was created when Asset {0} was {1} through Sales Invoice {2}.").format(
get_link_to_form(asset.doctype, asset.name),
_("returned") if self.is_return else _("sold"),
get_link_to_form(self.doctype, self.get("name")),
)
def restore_asset(self):
"""
Restore asset on return or cancellation of original sales invoice
"""
for d in self.get("items"):
if d.asset:
asset = frappe.get_cached_doc("Asset", d.asset)
if asset.calculate_depreciation:
reverse_depreciation_entry_made_on_disposal(asset)
note = self.get_note_for_asset_return(asset)
reset_depreciation_schedule(asset, note)
def get_note_for_asset_return(self, asset):
asset_link = get_link_to_form(asset.doctype, asset.name)
invoice_link = get_link_to_form(self.doctype, self.get("name"))
if self.is_return:
return _(
"This schedule was created when Asset {0} was returned through Sales Invoice {1}."
).format(asset_link, invoice_link)
else:
return _(
"This schedule was created when Asset {0} was restored due to Sales Invoice {1} cancellation."
).format(asset_link, invoice_link)
def update_asset(self):
"""
Update asset status, disposal date and asset activity on sale or return sales invoice
"""
def _update_asset(asset, disposal_date, note, asset_status=None):
frappe.db.set_value("Asset", d.asset, "disposal_date", disposal_date)
add_asset_activity(asset.name, note)
asset.set_status(asset_status)
disposal_date = self.get_disposal_date()
for d in self.get("items"):
if d.asset:
asset = frappe.get_cached_doc("Asset", d.asset)
if (self.is_return and self.docstatus == 1) or (not self.is_return and self.docstatus == 2):
note = _("Asset returned") if self.is_return else _("Asset sold")
asset_status, disposal_date = None, None
else:
note = _("Asset sold") if not self.is_return else _("Return invoice of asset cancelled")
asset_status = "Sold"
_update_asset(asset, disposal_date, note, asset_status)
def get_disposal_date(self):
if self.is_return:
disposal_date = frappe.db.get_value("Sales Invoice", self.return_against, "posting_date")
else:
disposal_date = self.posting_date
return disposal_date
def make_gl_entries(self, gl_entries=None, from_repost=False):
from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries
@@ -1426,64 +1527,8 @@ class SalesInvoice(SellingController):
if self.is_internal_transfer():
continue
if item.is_fixed_asset:
asset = self.get_asset(item)
if self.is_return:
fixed_asset_gl_entries = get_gl_entries_on_asset_regain(
asset,
item.base_net_amount,
item.finance_book,
self.get("doctype"),
self.get("name"),
self.get("posting_date"),
)
asset.db_set("disposal_date", None)
add_asset_activity(asset.name, _("Asset returned"))
if asset.calculate_depreciation:
posting_date = frappe.db.get_value(
"Sales Invoice", self.return_against, "posting_date"
)
reverse_depreciation_entry_made_after_disposal(asset, posting_date)
notes = _(
"This schedule was created when Asset {0} was returned through Sales Invoice {1}."
).format(
get_link_to_form(asset.doctype, asset.name),
get_link_to_form(self.doctype, self.get("name")),
)
reset_depreciation_schedule(asset, self.posting_date, notes)
asset.reload()
else:
if asset.calculate_depreciation:
if not asset.status == "Fully Depreciated":
notes = _(
"This schedule was created when Asset {0} was sold through Sales Invoice {1}."
).format(
get_link_to_form(asset.doctype, asset.name),
get_link_to_form(self.doctype, self.get("name")),
)
depreciate_asset(asset, self.posting_date, notes)
asset.reload()
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
asset,
item.base_net_amount,
item.finance_book,
self.get("doctype"),
self.get("name"),
self.get("posting_date"),
)
asset.db_set("disposal_date", self.posting_date)
add_asset_activity(asset.name, _("Asset sold"))
for gle in fixed_asset_gl_entries:
gle["against"] = self.customer
gl_entries.append(self.get_gl_dict(gle, item=item))
self.set_asset_status(asset)
if item.is_fixed_asset and item.asset:
self.get_gl_entries_for_fixed_asset(item, gl_entries)
else:
income_account = (
item.income_account
@@ -1518,17 +1563,31 @@ class SalesInvoice(SellingController):
if cint(self.update_stock) and erpnext.is_perpetual_inventory_enabled(self.company):
gl_entries += super().get_gl_entries()
def get_asset(self, item):
if item.get("asset"):
asset = frappe.get_doc("Asset", item.asset)
def get_gl_entries_for_fixed_asset(self, item, gl_entries):
asset = frappe.get_cached_doc("Asset", item.asset)
if self.is_return:
fixed_asset_gl_entries = get_gl_entries_on_asset_regain(
asset,
item.base_net_amount,
item.finance_book,
self.get("doctype"),
self.get("name"),
self.get("posting_date"),
)
else:
frappe.throw(
_("Row #{0}: You must select an Asset for Item {1}.").format(item.idx, item.item_name),
title=_("Missing Asset"),
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
asset,
item.base_net_amount,
item.finance_book,
self.get("doctype"),
self.get("name"),
self.get("posting_date"),
)
self.check_finance_books(item, asset)
return asset
for gle in fixed_asset_gl_entries:
gle["against"] = self.customer
gl_entries.append(self.get_gl_dict(gle, item=item))
@property
def enable_discount_accounting(self):
@@ -1539,12 +1598,6 @@ class SalesInvoice(SellingController):
return self._enable_discount_accounting
def set_asset_status(self, asset):
if self.is_return:
asset.set_status()
else:
asset.set_status("Sold" if self.docstatus == 1 else None)
def make_loyalty_point_redemption_gle(self, gl_entries):
if cint(self.redeem_loyalty_points and self.loyalty_points and not self.is_consolidated):
gl_entries.append(

View File

@@ -2979,7 +2979,7 @@ class TestSalesInvoice(ERPNextTestSuite):
expected_values = [
["2020-06-30", 1366.12, 1366.12],
["2021-06-30", 20000.0, 21366.12],
["2021-09-30", 5041.1, 26407.22],
["2021-09-30", 5041.34, 26407.46],
]
for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")):
@@ -3011,7 +3011,7 @@ class TestSalesInvoice(ERPNextTestSuite):
)
asset.load_from_db()
expected_values = [["2020-12-31", 30000, 30000], ["2021-12-31", 30000, 60000]]
expected_values = [["2020-12-31", 30000, 30000], ["2021-12-31", 30041.15, 60041.15]]
for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")):
self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
@@ -3019,35 +3019,6 @@ class TestSalesInvoice(ERPNextTestSuite):
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
self.assertTrue(schedule.journal_entry)
def test_depreciation_on_return_of_sold_asset(self):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
create_asset_data()
asset = create_asset(item_code="Macbook Pro", calculate_depreciation=1, submit=1)
post_depreciation_entries(getdate("2021-09-30"))
si = create_sales_invoice(
item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000, posting_date=getdate("2021-09-30")
)
return_si = make_return_doc("Sales Invoice", si.name)
return_si.submit()
asset.load_from_db()
expected_values = [
["2020-06-30", 1366.12, 1366.12, True],
["2021-06-30", 20000.0, 21366.12, True],
["2022-06-30", 20000.0, 41366.12, False],
["2023-06-30", 20000.0, 61366.12, False],
["2024-06-30", 20000.0, 81366.12, False],
["2025-06-06", 18633.88, 100000.0, False],
]
for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")):
self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
self.assertEqual(expected_values[i][1], schedule.depreciation_amount)
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
self.assertEqual(schedule.journal_entry, schedule.journal_entry)
def test_sales_invoice_against_supplier(self):
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
make_customer,

View File

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

View File

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

View File

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

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 (
get_asset_depr_schedule_doc,
get_asset_depr_schedule_name,
get_temp_asset_depr_schedule_doc,
make_new_active_asset_depr_schedules_and_cancel_current_ones,
get_temp_depr_schedule_doc,
reschedule_depreciation,
)
@@ -40,72 +40,43 @@ def post_depreciation_entries(date=None):
):
return
if not date:
date = today()
date = date or today()
book_depreciation_entries(date)
failed_asset_names = []
error_log_names = []
depreciable_asset_depr_schedules_data = get_depreciable_asset_depr_schedules_data(date)
credit_and_debit_accounts_for_asset_category_and_company = {}
depreciation_cost_center_and_depreciation_series_for_company = (
get_depreciation_cost_center_and_depreciation_series_for_company()
)
def book_depreciation_entries(date):
# Process depreciation entries for all depreciable assets
failed_assets, error_logs = [], []
depreciable_assets_data = get_depreciable_assets_data(date)
accounting_dimensions = get_checks_for_pl_and_bs_accounts()
for asset_depr_schedule_data in depreciable_asset_depr_schedules_data:
(
asset_depr_schedule_name,
asset_name,
asset_category,
asset_company,
sch_start_idx,
sch_end_idx,
) = asset_depr_schedule_data
if (
asset_category,
asset_company,
) not in credit_and_debit_accounts_for_asset_category_and_company:
credit_and_debit_accounts_for_asset_category_and_company.update(
{
(
asset_category,
asset_company,
): get_credit_and_debit_accounts_for_asset_category_and_company(
asset_category, asset_company
),
}
)
for data in depreciable_assets_data:
(depr_schedule_name, asset_name, sch_start_idx, sch_end_idx) = data
try:
make_depreciation_entry(
asset_depr_schedule_name,
depr_schedule_name,
date,
sch_start_idx,
sch_end_idx,
credit_and_debit_accounts_for_asset_category_and_company[(asset_category, asset_company)],
depreciation_cost_center_and_depreciation_series_for_company[asset_company],
accounting_dimensions,
)
frappe.db.commit()
except Exception as e:
frappe.db.rollback()
failed_asset_names.append(asset_name)
failed_assets.append(asset_name)
error_log = frappe.log_error(e)
error_log_names.append(error_log.name)
if failed_asset_names:
set_depr_entry_posting_status_for_failed_assets(failed_asset_names)
notify_depr_entry_posting_error(failed_asset_names, error_log_names)
error_logs.append(error_log.name)
if failed_assets:
set_depr_entry_posting_status_for_failed_assets(failed_assets)
notify_depr_entry_posting_error(failed_assets, error_logs)
frappe.db.commit()
def get_depreciable_asset_depr_schedules_data(date):
def get_depreciable_assets_data(date):
a = frappe.qb.DocType("Asset")
ads = frappe.qb.DocType("Asset Depreciation Schedule")
ds = frappe.qb.DocType("Depreciation Schedule")
@@ -116,7 +87,7 @@ def get_depreciable_asset_depr_schedules_data(date):
.on(ads.asset == a.name)
.join(ds)
.on(ads.name == ds.parent)
.select(ads.name, a.name, a.asset_category, a.company, Min(ds.idx) - 1, Max(ds.idx))
.select(ads.name, a.name, Min(ds.idx) - 1, Max(ds.idx))
.where(a.calculate_depreciation == 1)
.where(a.docstatus == 1)
.where(ads.docstatus == 1)
@@ -136,10 +107,10 @@ def get_depreciable_asset_depr_schedules_data(date):
return res
def make_depreciation_entry_for_all_asset_depr_schedules(asset_doc, date=None):
def make_depreciation_entry_on_disposal(asset_doc, disposal_date=None):
for row in asset_doc.get("finance_books"):
asset_depr_schedule_name = get_asset_depr_schedule_name(asset_doc.name, "Active", row.finance_book)
make_depreciation_entry(asset_depr_schedule_name, date)
depr_schedule_name = get_asset_depr_schedule_name(asset_doc.name, "Active", row.finance_book)
make_depreciation_entry(depr_schedule_name, disposal_date)
def get_acc_frozen_upto():
@@ -156,21 +127,26 @@ def get_acc_frozen_upto():
return
def get_credit_and_debit_accounts_for_asset_category_and_company(asset_category, company):
(
_,
accumulated_depreciation_account,
depreciation_expense_account,
) = get_depreciation_accounts(asset_category, company)
def get_credit_debit_accounts_for_asset(asset_category, company):
# Returns credit and debit accounts for the given asset category and company.
(_, accumulated_depr_account, depr_expense_account) = get_depreciation_accounts(asset_category, company)
credit_account, debit_account = get_credit_and_debit_accounts(
accumulated_depreciation_account, depreciation_expense_account
accumulated_depr_account, depr_expense_account
)
return (credit_account, debit_account)
def get_depreciation_cost_center_and_depreciation_series_for_company():
def get_depreciation_cost_center_and_series(asset):
depreciation_cost_center, depreciation_series = frappe.get_cached_value(
"Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"]
)
depreciation_cost_center = asset.cost_center or depreciation_cost_center
return depreciation_cost_center, depreciation_series
def get_depr_cost_center_and_series():
company_names = frappe.db.get_all("Company", pluck="name")
res = {}
@@ -179,89 +155,70 @@ def get_depreciation_cost_center_and_depreciation_series_for_company():
depreciation_cost_center, depreciation_series = frappe.get_cached_value(
"Company", company_name, ["depreciation_cost_center", "series_for_depreciation_entry"]
)
res.update({company_name: (depreciation_cost_center, depreciation_series)})
res.setdefault(company_name, (depreciation_cost_center, depreciation_series))
return res
@frappe.whitelist()
def make_depreciation_entry(
asset_depr_schedule_name,
depr_schedule_name,
date=None,
sch_start_idx=None,
sch_end_idx=None,
credit_and_debit_accounts=None,
depreciation_cost_center_and_depreciation_series=None,
accounting_dimensions=None,
):
frappe.has_permission("Journal Entry", throw=True)
date = date or today()
if not date:
date = today()
depr_schedule_doc = frappe.get_doc("Asset Depreciation Schedule", depr_schedule_name)
asset = frappe.get_doc("Asset", depr_schedule_doc.asset)
asset_depr_schedule_doc = frappe.get_doc("Asset Depreciation Schedule", asset_depr_schedule_name)
credit_account, debit_account = get_credit_debit_accounts_for_asset(asset.asset_category, asset.company)
depr_cost_center, depr_series = get_depreciation_cost_center_and_series(asset)
accounting_dimensions = accounting_dimensions or get_checks_for_pl_and_bs_accounts()
depr_posting_error = None
asset = frappe.get_doc("Asset", asset_depr_schedule_doc.asset)
if credit_and_debit_accounts:
credit_account, debit_account = credit_and_debit_accounts
else:
credit_account, debit_account = get_credit_and_debit_accounts_for_asset_category_and_company(
asset.asset_category, asset.company
)
if depreciation_cost_center_and_depreciation_series:
depreciation_cost_center, depreciation_series = depreciation_cost_center_and_depreciation_series
else:
depreciation_cost_center, depreciation_series = frappe.get_cached_value(
"Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"]
)
depreciation_cost_center = asset.cost_center or depreciation_cost_center
if not accounting_dimensions:
accounting_dimensions = get_checks_for_pl_and_bs_accounts()
depreciation_posting_error = None
for d in asset_depr_schedule_doc.get("depreciation_schedule")[
sch_start_idx or 0 : sch_end_idx or len(asset_depr_schedule_doc.get("depreciation_schedule"))
for d in depr_schedule_doc.get("depreciation_schedule")[
(sch_start_idx or 0) : (sch_end_idx or len(depr_schedule_doc.get("depreciation_schedule")))
]:
try:
_make_journal_entry_for_depreciation(
asset_depr_schedule_doc,
depr_schedule_doc,
asset,
date,
d,
sch_start_idx,
sch_end_idx,
depreciation_cost_center,
depreciation_series,
depr_cost_center,
depr_series,
credit_account,
debit_account,
accounting_dimensions,
)
except Exception as e:
depreciation_posting_error = e
depr_posting_error = e
asset.reload()
asset.set_status()
if not depreciation_posting_error:
if not depr_posting_error:
asset.db_set("depr_entry_posting_status", "Successful")
return asset_depr_schedule_doc
depr_schedule_doc.reload()
return depr_schedule_doc
raise depreciation_posting_error
raise depr_posting_error
def _make_journal_entry_for_depreciation(
asset_depr_schedule_doc,
depr_schedule_doc,
asset,
date,
depr_schedule,
sch_start_idx,
sch_end_idx,
depreciation_cost_center,
depreciation_series,
depr_cost_center,
depr_series,
credit_account,
debit_account,
accounting_dimensions,
@@ -272,19 +229,40 @@ def _make_journal_entry_for_depreciation(
return
je = frappe.new_doc("Journal Entry")
setup_journal_entry_metadata(je, depr_schedule_doc, depr_series, depr_schedule, asset)
credit_entry, debit_entry = get_credit_and_debit_entry(
credit_account, depr_schedule, asset, depr_cost_center, debit_account, accounting_dimensions
)
je.append("accounts", credit_entry)
je.append("accounts", debit_entry)
je.flags.ignore_permissions = True
je.save()
if not je.meta.get_workflow():
je.submit()
def setup_journal_entry_metadata(je, depr_schedule_doc, depr_series, depr_schedule, asset):
je.voucher_type = "Depreciation Entry"
je.naming_series = depreciation_series
je.naming_series = depr_series
je.posting_date = depr_schedule.schedule_date
je.company = asset.company
je.finance_book = asset_depr_schedule_doc.finance_book
je.finance_book = depr_schedule_doc.finance_book
je.remark = f"Depreciation Entry against {asset.name} worth {depr_schedule.depreciation_amount}"
def get_credit_and_debit_entry(
credit_account, depr_schedule, asset, depr_cost_center, debit_account, dimensions
):
credit_entry = {
"account": credit_account,
"credit_in_account_currency": depr_schedule.depreciation_amount,
"reference_type": "Asset",
"reference_name": asset.name,
"cost_center": depreciation_cost_center,
"cost_center": depr_cost_center,
}
debit_entry = {
@@ -292,42 +270,20 @@ def _make_journal_entry_for_depreciation(
"debit_in_account_currency": depr_schedule.depreciation_amount,
"reference_type": "Asset",
"reference_name": asset.name,
"cost_center": depreciation_cost_center,
"cost_center": depr_cost_center,
}
for dimension in accounting_dimensions:
for dimension in dimensions:
if asset.get(dimension["fieldname"]) or dimension.get("mandatory_for_bs"):
credit_entry.update(
{
dimension["fieldname"]: asset.get(dimension["fieldname"])
or dimension.get("default_dimension")
}
credit_entry[dimension["fieldname"]] = asset.get(dimension["fieldname"]) or dimension.get(
"default_dimension"
)
if asset.get(dimension["fieldname"]) or dimension.get("mandatory_for_pl"):
debit_entry.update(
{
dimension["fieldname"]: asset.get(dimension["fieldname"])
or dimension.get("default_dimension")
}
debit_entry[dimension["fieldname"]] = asset.get(dimension["fieldname"]) or dimension.get(
"default_dimension"
)
je.append("accounts", credit_entry)
je.append("accounts", debit_entry)
je.flags.ignore_permissions = True
je.flags.planned_depr_entry = True
je.save()
depr_schedule.db_set("journal_entry", je.name)
if not je.meta.get_workflow():
je.submit()
asset.reload()
idx = cint(asset_depr_schedule_doc.finance_book_id)
row = asset.get("finance_books")[idx - 1]
row.value_after_depreciation -= depr_schedule.depreciation_amount
row.db_update()
return credit_entry, debit_entry
def get_depreciation_accounts(asset_category, company):
@@ -400,21 +356,7 @@ def notify_depr_entry_posting_error(failed_asset_names, error_log_names):
asset_links = get_comma_separated_links(failed_asset_names, "Asset")
error_log_links = get_comma_separated_links(error_log_names, "Error Log")
message = (
_("Hello,")
+ "<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.")
)
message = get_message_for_depr_entry_posting_error(asset_links, error_log_links)
frappe.sendmail(recipients=recipients, subject=subject, message=message)
@@ -430,197 +372,184 @@ def get_comma_separated_links(names, doctype):
return links
def get_message_for_depr_entry_posting_error(asset_links, error_log_links):
return (
_("Hello,")
+ "<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()
def scrap_asset(asset_name, scrap_date=None):
asset = frappe.get_doc("Asset", asset_name)
scrap_date = getdate(scrap_date) or getdate(today())
asset.db_set("disposal_date", scrap_date)
validate_asset_for_scrap(asset, scrap_date)
depreciate_asset(asset, scrap_date, get_note_for_scrap(asset))
asset.reload()
create_journal_entry_for_scrap(asset, scrap_date)
def validate_asset_for_scrap(asset, scrap_date):
if asset.docstatus != 1:
frappe.throw(_("Asset {0} must be submitted").format(asset.name))
elif asset.status in ("Cancelled", "Sold", "Scrapped", "Capitalized"):
frappe.throw(_("Asset {0} cannot be scrapped, as it is already {1}").format(asset.name, asset.status))
today_date = getdate(today())
date = getdate(scrap_date) or today_date
purchase_date = getdate(asset.purchase_date)
validate_scrap_date(asset, scrap_date)
validate_scrap_date(date, today_date, purchase_date, asset.calculate_depreciation, asset_name)
notes = _("This schedule was created when Asset {0} was scrapped.").format(
def validate_scrap_date(asset, scrap_date):
if scrap_date > getdate():
frappe.throw(_("Future date is not allowed"))
elif scrap_date < getdate(asset.purchase_date):
frappe.throw(_("Scrap date cannot be before purchase date"))
if asset.calculate_depreciation:
last_booked_depreciation_date = get_last_depreciation_date(asset.name)
if (
last_booked_depreciation_date
and scrap_date < last_booked_depreciation_date
and scrap_date > getdate(asset.purchase_date)
):
frappe.throw(_("Asset cannot be scrapped before the last depreciation entry."))
def get_last_depreciation_date(asset_name):
depreciation = frappe.qb.DocType("Asset Depreciation Schedule")
depreciation_schedule = frappe.qb.DocType("Depreciation Schedule")
last_depreciation_date = (
frappe.qb.from_(depreciation)
.join(depreciation_schedule)
.on(depreciation.name == depreciation_schedule.parent)
.select(depreciation_schedule.schedule_date)
.where(depreciation.asset == asset_name)
.where(depreciation.docstatus == 1)
.where(depreciation_schedule.journal_entry != "")
.orderby(depreciation_schedule.schedule_date, order=Order.desc)
.limit(1)
.run()
)
return last_depreciation_date[0][0] if last_depreciation_date else None
def get_note_for_scrap(asset):
return _("This schedule was created when Asset {0} was scrapped.").format(
get_link_to_form(asset.doctype, asset.name)
)
if asset.status != "Fully Depreciated":
depreciate_asset(asset, date, notes)
asset.reload()
def create_journal_entry_for_scrap(asset, scrap_date):
depreciation_series = frappe.get_cached_value("Company", asset.company, "series_for_depreciation_entry")
je = frappe.new_doc("Journal Entry")
je.voucher_type = "Journal Entry"
je.voucher_type = "Asset Disposal"
je.naming_series = depreciation_series
je.posting_date = date
je.posting_date = scrap_date
je.company = asset.company
je.remark = f"Scrap Entry for asset {asset_name}"
je.remark = f"Scrap Entry for asset {asset.name}"
for entry in get_gl_entries_on_asset_disposal(asset, date):
entry.update({"reference_type": "Asset", "reference_name": asset_name})
for entry in get_gl_entries_on_asset_disposal(asset, scrap_date):
entry.update({"reference_type": "Asset", "reference_name": asset.name})
je.append("accounts", entry)
je.flags.ignore_permissions = True
je.submit()
je.save()
if not je.meta.get_workflow():
je.submit()
frappe.db.set_value("Asset", asset_name, "disposal_date", date)
frappe.db.set_value("Asset", asset_name, "journal_entry_for_scrap", je.name)
asset.set_status("Scrapped")
add_asset_activity(asset_name, _("Asset scrapped"))
frappe.msgprint(_("Asset scrapped via Journal Entry {0}").format(je.name))
def validate_scrap_date(scrap_date, today_date, purchase_date, calculate_depreciation, asset_name):
if scrap_date > today_date:
frappe.throw(_("Future date is not allowed"))
elif scrap_date < purchase_date:
frappe.throw(_("Scrap date cannot be before purchase date"))
if calculate_depreciation:
asset_depreciation_schedules = frappe.db.get_all(
"Asset Depreciation Schedule", filters={"asset": asset_name, "docstatus": 1}, fields=["name"]
)
for depreciation_schedule in asset_depreciation_schedules:
last_booked_depreciation_date = frappe.db.get_value(
"Depreciation Schedule",
{
"parent": depreciation_schedule["name"],
"docstatus": 1,
"journal_entry": ["!=", ""],
},
"schedule_date",
order_by="schedule_date desc",
)
if (
last_booked_depreciation_date
and scrap_date < last_booked_depreciation_date
and scrap_date > purchase_date
):
frappe.throw(_("Asset cannot be scrapped before the last depreciation entry."))
add_asset_activity(asset.name, _("Asset scrapped"))
frappe.msgprint(
_("Asset scrapped via Journal Entry {0}").format(get_link_to_form("Journal Entry", je.name))
)
@frappe.whitelist()
def restore_asset(asset_name):
asset = frappe.get_doc("Asset", asset_name)
reverse_depreciation_entry_made_on_disposal(asset)
reset_depreciation_schedule(asset, get_note_for_restore(asset))
cancel_journal_entry_for_scrap(asset)
asset.set_status()
add_asset_activity(asset_name, _("Asset restored"))
reverse_depreciation_entry_made_after_disposal(asset, asset.disposal_date)
je = asset.journal_entry_for_scrap
notes = _("This schedule was created when Asset {0} was restored.").format(
def get_note_for_restore(asset):
return _("This schedule was created when Asset {0} was restored.").format(
get_link_to_form(asset.doctype, asset.name)
)
reset_depreciation_schedule(asset, asset.disposal_date, notes)
asset.db_set("disposal_date", None)
asset.db_set("journal_entry_for_scrap", None)
frappe.get_doc("Journal Entry", je).cancel()
asset.set_status()
add_asset_activity(asset_name, _("Asset restored"))
def cancel_journal_entry_for_scrap(asset):
if asset.journal_entry_for_scrap:
je = asset.journal_entry_for_scrap
asset.db_set("disposal_date", None)
asset.db_set("journal_entry_for_scrap", None)
frappe.get_doc("Journal Entry", je).cancel()
def depreciate_asset(asset_doc, date, notes):
if not asset_doc.calculate_depreciation:
return
asset_doc.flags.ignore_validate_update_after_submit = True
make_new_active_asset_depr_schedules_and_cancel_current_ones(asset_doc, notes, date_of_disposal=date)
asset_doc.save()
make_depreciation_entry_for_all_asset_depr_schedules(asset_doc, date)
reschedule_depreciation(asset_doc, notes, disposal_date=date)
make_depreciation_entry_on_disposal(asset_doc, date)
# As per Income Tax Act (India), the asset should not be depreciated
# in the financial year in which it is sold/scraped
asset_doc.reload()
cancel_depreciation_entries(asset_doc, date)
@erpnext.allow_regional
def cancel_depreciation_entries(asset_doc, date):
# Cancel all depreciation entries for the current financial year
# if the asset is sold/scraped in the current financial year
# Overwritten via India Compliance app
pass
def reset_depreciation_schedule(asset_doc, date, notes):
if not asset_doc.calculate_depreciation:
return
asset_doc.flags.ignore_validate_update_after_submit = True
make_new_active_asset_depr_schedules_and_cancel_current_ones(asset_doc, notes, date_of_return=date)
modify_depreciation_schedule_for_asset_repairs(asset_doc, notes)
asset_doc.save()
def reset_depreciation_schedule(asset_doc, notes):
if asset_doc.calculate_depreciation:
reschedule_depreciation(asset_doc, notes)
asset_doc.set_total_booked_depreciations()
def modify_depreciation_schedule_for_asset_repairs(asset, notes):
asset_repairs = frappe.get_all(
"Asset Repair", filters={"asset": asset.name}, fields=["name", "increase_in_asset_life"]
)
for repair in asset_repairs:
if repair.increase_in_asset_life:
asset_repair = frappe.get_doc("Asset Repair", repair.name)
asset_repair.modify_depreciation_schedule()
make_new_active_asset_depr_schedules_and_cancel_current_ones(asset, notes)
def reverse_depreciation_entry_made_after_disposal(asset, date):
def reverse_depreciation_entry_made_on_disposal(asset):
for row in asset.get("finance_books"):
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active", row.finance_book)
if not asset_depr_schedule_doc or not asset_depr_schedule_doc.get("depreciation_schedule"):
schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active", row.finance_book)
if not schedule_doc or not schedule_doc.get("depreciation_schedule"):
continue
for schedule_idx, schedule in enumerate(asset_depr_schedule_doc.get("depreciation_schedule")):
if schedule.schedule_date == date and schedule.journal_entry:
for schedule_idx, schedule in enumerate(schedule_doc.get("depreciation_schedule")):
if schedule.schedule_date == asset.disposal_date and schedule.journal_entry:
if not disposal_was_made_on_original_schedule_date(
schedule_idx, row, date
) or disposal_happens_in_the_future(date):
reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry)
reverse_journal_entry.posting_date = nowdate()
for account in reverse_journal_entry.accounts:
account.update(
{
"reference_type": "Asset",
"reference_name": asset.name,
}
)
frappe.flags.is_reverse_depr_entry = True
reverse_journal_entry.submit()
frappe.flags.is_reverse_depr_entry = False
asset_depr_schedule_doc.flags.ignore_validate_update_after_submit = True
asset.flags.ignore_validate_update_after_submit = True
schedule.journal_entry = None
depreciation_amount = get_depreciation_amount_in_je(reverse_journal_entry)
row.value_after_depreciation += depreciation_amount
asset_depr_schedule_doc.save()
asset.save()
schedule_idx, row, asset.disposal_date
) or disposal_happens_in_the_future(asset.disposal_date):
je = create_reverse_depreciation_entry(asset.name, schedule.journal_entry)
update_value_after_depreciation_on_asset_restore(schedule, row, je)
def get_depreciation_amount_in_je(journal_entry):
if journal_entry.accounts[0].debit_in_account_currency:
return journal_entry.accounts[0].debit_in_account_currency
else:
return journal_entry.accounts[0].credit_in_account_currency
# if the invoice had been posted on the date the depreciation was initially supposed to happen, the depreciation shouldn't be undone
def disposal_was_made_on_original_schedule_date(schedule_idx, row, posting_date_of_disposal):
def disposal_was_made_on_original_schedule_date(schedule_idx, row, disposal_date):
"""
If asset is scrapped or sold on original schedule date,
then the depreciation entry should not be reversed.
"""
orginal_schedule_date = add_months(
row.depreciation_start_date, schedule_idx * cint(row.frequency_of_depreciation)
)
@@ -628,19 +557,57 @@ def disposal_was_made_on_original_schedule_date(schedule_idx, row, posting_date_
if is_last_day_of_the_month(row.depreciation_start_date):
orginal_schedule_date = get_last_day(orginal_schedule_date)
if orginal_schedule_date == posting_date_of_disposal:
if orginal_schedule_date == disposal_date:
return True
return False
def disposal_happens_in_the_future(posting_date_of_disposal):
if posting_date_of_disposal > getdate():
def disposal_happens_in_the_future(disposal_date):
if disposal_date > getdate():
return True
return False
def create_reverse_depreciation_entry(asset_name, journal_entry):
reverse_journal_entry = make_reverse_journal_entry(journal_entry)
reverse_journal_entry.posting_date = nowdate()
for account in reverse_journal_entry.accounts:
account.update(
{
"reference_type": "Asset",
"reference_name": asset_name,
}
)
frappe.flags.is_reverse_depr_entry = True
if not reverse_journal_entry.meta.get_workflow():
reverse_journal_entry.submit()
return reverse_journal_entry
else:
frappe.throw(
_("Please disable workflow temporarily for Journal Entry {0}").format(reverse_journal_entry.name)
)
def update_value_after_depreciation_on_asset_restore(schedule, row, journal_entry):
frappe.db.set_value("Depreciation Schedule", schedule.name, "journal_entry", None, update_modified=False)
depreciation_amount = get_depreciation_amount_in_je(journal_entry)
value_after_depreciation = flt(
row.value_after_depreciation + depreciation_amount, row.precision("value_after_depreciation")
)
row.db_set("value_after_depreciation", value_after_depreciation)
def get_depreciation_amount_in_je(journal_entry):
if journal_entry.accounts[0].debit_in_account_currency:
return journal_entry.accounts[0].debit_in_account_currency
else:
return journal_entry.accounts[0].credit_in_account_currency
def get_gl_entries_on_asset_regain(
asset, selling_amount=0, finance_book=None, voucher_type=None, voucher_no=None, date=None
):
@@ -874,9 +841,7 @@ def get_value_after_depreciation_on_disposal_date(asset, disposal_date, finance_
row = asset_doc.finance_books[idx - 1]
temp_asset_depreciation_schedule = get_temp_asset_depr_schedule_doc(
asset_doc, row, getdate(disposal_date)
)
temp_asset_depreciation_schedule = get_temp_depr_schedule_doc(asset_doc, row, getdate(disposal_date))
accumulated_depr_amount = temp_asset_depreciation_schedule.get("depreciation_schedule")[
-1

View File

@@ -30,11 +30,8 @@ from erpnext.assets.doctype.asset.depreciation import (
scrap_asset,
)
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
_check_is_pro_rata,
_get_pro_rata_amt,
get_asset_depr_schedule_doc,
get_depr_schedule,
get_depreciation_amount,
)
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
make_purchase_invoice as make_invoice,
@@ -196,7 +193,7 @@ class TestAsset(AssetSetup):
self.assertEqual(doc.items[0].is_fixed_asset, 1)
def test_scrap_asset(self):
date = nowdate()
date = "2025-05-05"
purchase_date = add_months(get_first_day(date), -2)
asset = create_asset(
@@ -246,7 +243,7 @@ class TestAsset(AssetSetup):
frappe.ValidationError, scrap_asset, asset.name, scrap_date=before_last_booked_depreciation_date
)
scrap_asset(asset.name)
scrap_asset(asset.name, date)
asset.load_from_db()
first_asset_depr_schedule.load_from_db()
@@ -258,12 +255,16 @@ class TestAsset(AssetSetup):
asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation,
asset.precision("gross_purchase_amount"),
)
pro_rata_amount, _, _ = _get_pro_rata_amt(
asset.finance_books[0],
9000,
second_asset_depr_schedule.depreciation_amount = 9006.17
second_asset_depr_schedule.asset_doc = asset
second_asset_depr_schedule.get_finance_book_row()
second_asset_depr_schedule.fetch_asset_details()
pro_rata_amount, _, _ = second_asset_depr_schedule._get_pro_rata_amt(
add_days(get_last_day(add_months(purchase_date, 1)), 1),
date,
original_schedule_date=get_last_day(nowdate()),
original_schedule_date=get_last_day(date),
)
pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount"))
self.assertEqual(
@@ -331,7 +332,7 @@ class TestAsset(AssetSetup):
si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company")
si.customer = "_Test Customer"
si.due_date = nowdate()
si.due_date = date
si.get("items")[0].rate = 25000
si.insert()
si.submit()
@@ -344,19 +345,17 @@ class TestAsset(AssetSetup):
self.assertEqual(second_asset_depr_schedule.status, "Active")
self.assertEqual(first_asset_depr_schedule.status, "Cancelled")
pro_rata_amount, _, _ = _get_pro_rata_amt(
asset.finance_books[0],
9000,
add_days(get_last_day(add_months(purchase_date, 1)), 1),
date,
original_schedule_date=get_last_day(nowdate()),
asset.load_from_db()
accumulated_depr_amount = flt(
asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation,
asset.precision("gross_purchase_amount"),
)
pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount"))
pro_rata_amount = flt(accumulated_depr_amount - 18000)
expected_gle = (
(
"_Test Accumulated Depreciations - _TC",
flt(18000.0 + pro_rata_amount, asset.precision("gross_purchase_amount")),
flt(accumulated_depr_amount, asset.precision("gross_purchase_amount")),
0.0,
),
("_Test Fixed Asset - _TC", 0.0, 100000.0),
@@ -476,26 +475,26 @@ class TestAsset(AssetSetup):
asset = create_asset(
calculate_depreciation=1,
asset_quantity=10,
available_for_use_date="2020-01-01",
purchase_date="2020-01-01",
available_for_use_date="2023-01-01",
purchase_date="2023-01-01",
expected_value_after_useful_life=0,
total_number_of_depreciations=6,
opening_number_of_booked_depreciations=1,
frequency_of_depreciation=10,
depreciation_start_date="2021-01-01",
opening_accumulated_depreciation=20000,
gross_purchase_amount=120000,
frequency_of_depreciation=12,
depreciation_start_date="2024-03-31",
opening_accumulated_depreciation=493.15,
gross_purchase_amount=12000,
submit=1,
)
first_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active")
self.assertEqual(first_asset_depr_schedule.status, "Active")
post_depreciation_entries(date="2021-01-01")
post_depreciation_entries(date="2024-03-31")
self.assertEqual(asset.asset_quantity, 10)
self.assertEqual(asset.gross_purchase_amount, 120000)
self.assertEqual(asset.opening_accumulated_depreciation, 20000)
self.assertEqual(asset.gross_purchase_amount, 12000)
self.assertEqual(asset.opening_accumulated_depreciation, 493.15)
new_asset = split_asset(asset.name, 2)
asset.load_from_db()
@@ -511,25 +510,25 @@ class TestAsset(AssetSetup):
depr_schedule_of_new_asset = first_asset_depr_schedule_of_new_asset.get("depreciation_schedule")
self.assertEqual(new_asset.asset_quantity, 2)
self.assertEqual(new_asset.gross_purchase_amount, 24000)
self.assertEqual(new_asset.opening_accumulated_depreciation, 4000)
self.assertEqual(new_asset.gross_purchase_amount, 2400)
self.assertEqual(new_asset.opening_accumulated_depreciation, 98.63)
self.assertEqual(new_asset.split_from, asset.name)
self.assertEqual(depr_schedule_of_new_asset[0].depreciation_amount, 4000)
self.assertEqual(depr_schedule_of_new_asset[1].depreciation_amount, 4000)
self.assertEqual(depr_schedule_of_new_asset[0].depreciation_amount, 400)
self.assertEqual(depr_schedule_of_new_asset[1].depreciation_amount, 400)
self.assertEqual(asset.asset_quantity, 8)
self.assertEqual(asset.gross_purchase_amount, 96000)
self.assertEqual(asset.opening_accumulated_depreciation, 16000)
self.assertEqual(depr_schedule_of_asset[0].depreciation_amount, 16000)
self.assertEqual(depr_schedule_of_asset[1].depreciation_amount, 16000)
self.assertEqual(asset.gross_purchase_amount, 9600)
self.assertEqual(asset.opening_accumulated_depreciation, 394.52)
self.assertEqual(depr_schedule_of_asset[0].depreciation_amount, 1600)
self.assertEqual(depr_schedule_of_asset[1].depreciation_amount, 1600)
journal_entry = depr_schedule_of_asset[0].journal_entry
jv = frappe.get_doc("Journal Entry", journal_entry)
self.assertEqual(jv.accounts[0].credit_in_account_currency, 16000)
self.assertEqual(jv.accounts[1].debit_in_account_currency, 16000)
self.assertEqual(jv.accounts[2].credit_in_account_currency, 4000)
self.assertEqual(jv.accounts[3].debit_in_account_currency, 4000)
self.assertEqual(jv.accounts[0].credit_in_account_currency, 1600)
self.assertEqual(jv.accounts[1].debit_in_account_currency, 1600)
self.assertEqual(jv.accounts[2].credit_in_account_currency, 400)
self.assertEqual(jv.accounts[3].debit_in_account_currency, 400)
self.assertEqual(jv.accounts[0].reference_name, asset.name)
self.assertEqual(jv.accounts[1].reference_name, asset.name)
@@ -944,12 +943,12 @@ class TestDepreciationMethods(AssetSetup):
)
expected_schedules = [
["2022-02-28", 310.89, 310.89],
["2022-03-31", 654.45, 965.34],
["2022-04-30", 654.45, 1619.79],
["2022-05-31", 654.45, 2274.24],
["2022-06-30", 654.45, 2928.69],
["2022-07-15", 2071.31, 5000.0],
["2022-02-28", 337.72, 337.72],
["2022-03-31", 675.45, 1013.17],
["2022-04-30", 675.45, 1688.62],
["2022-05-31", 675.45, 2364.07],
["2022-06-30", 675.45, 3039.52],
["2022-07-15", 1960.48, 5000.0],
]
schedules = [
@@ -1024,12 +1023,17 @@ class TestDepreciationBasics(AssetSetup):
"depreciation_start_date": "2020-12-31",
},
)
asset.submit()
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active")
asset_depr_schedule_doc.asset_doc = asset
asset_depr_schedule_doc.get_finance_book_row()
asset_depr_schedule_doc.fetch_asset_details()
asset_depr_schedule_doc.clear()
asset_depr_schedule_doc._check_is_pro_rata()
asset_depr_schedule_doc.initialize_variables()
depreciation_amount, prev_per_day_depr = get_depreciation_amount(
asset_depr_schedule_doc, asset, 100000, 100000, asset.finance_books[0]
)
depreciation_amount = asset_depr_schedule_doc.get_depreciation_amount(0)
self.assertEqual(depreciation_amount, 30000)
def test_make_depr_schedule(self):
@@ -1074,7 +1078,7 @@ class TestDepreciationBasics(AssetSetup):
def test_check_is_pro_rata(self):
"""Tests if check_is_pro_rata() returns the right value(i.e. checks if has_pro_rata is accurate)."""
asset = create_asset(item_code="Macbook Pro", available_for_use_date="2019-12-31", do_not_save=1)
asset = create_asset(item_code="Macbook Pro", available_for_use_date="2019-12-31")
asset.calculate_depreciation = 1
asset.append(
@@ -1087,9 +1091,15 @@ class TestDepreciationBasics(AssetSetup):
"depreciation_start_date": "2020-12-31",
},
)
asset.save()
has_pro_rata = _check_is_pro_rata(asset, asset.finance_books[0])
self.assertFalse(has_pro_rata)
depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Draft")
depr_schedule_doc.asset_doc = asset
depr_schedule_doc.get_finance_book_row()
depr_schedule_doc.fetch_asset_details()
depr_schedule_doc._check_is_pro_rata()
self.assertFalse(depr_schedule_doc.has_pro_rata)
asset.finance_books = []
asset.append(
@@ -1102,9 +1112,15 @@ class TestDepreciationBasics(AssetSetup):
"depreciation_start_date": "2020-07-01",
},
)
asset.save()
has_pro_rata = _check_is_pro_rata(asset, asset.finance_books[0])
self.assertTrue(has_pro_rata)
depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Draft")
depr_schedule_doc.asset_doc = asset
depr_schedule_doc.get_finance_book_row()
depr_schedule_doc.fetch_asset_details()
depr_schedule_doc._check_is_pro_rata()
self.assertTrue(depr_schedule_doc.has_pro_rata)
def test_expected_value_after_useful_life_greater_than_purchase_amount(self):
"""Tests if an error is raised when expected_value_after_useful_life(110,000) > gross_purchase_amount(100,000)."""
@@ -1285,8 +1301,6 @@ class TestDepreciationBasics(AssetSetup):
self.assertFalse(entry["debit"])
def test_depr_entry_posting_when_depr_expense_account_is_an_income_account(self):
"""Tests if the Depreciation Expense Account gets credited and the Accumulated Depreciation Account gets debited when the former's an Income Account."""
depr_expense_account = frappe.get_doc("Account", "_Test Depreciations - _TC")
depr_expense_account.root_type = "Income"
depr_expense_account.parent_account = "Income - _TC"
@@ -1303,26 +1317,20 @@ class TestDepreciationBasics(AssetSetup):
submit=1,
)
post_depreciation_entries(date="2021-06-01")
asset.load_from_db()
jv = make_journal_entry(
"_Test Depreciations - _TC",
"_Test Accumulated Depreciations - _TC",
100,
posting_date="2020-01-15",
save=False,
)
for d in jv.accounts:
d.reference_type = "Asset"
d.reference_name = asset.name
jv.voucher_type = "Depreciation Entry"
je = frappe.get_doc("Journal Entry", get_depr_schedule(asset.name, "Active")[0].journal_entry)
accounting_entries = [
{"account": entry.account, "debit": entry.debit, "credit": entry.credit} for entry in je.accounts
]
for entry in accounting_entries:
if entry["account"] == "_Test Depreciations - _TC":
self.assertTrue(entry["credit"])
self.assertFalse(entry["debit"])
else:
self.assertTrue(entry["debit"])
self.assertFalse(entry["credit"])
# resetting
depr_expense_account.root_type = "Expense"
depr_expense_account.parent_account = "Expenses - _TC"
depr_expense_account.save()
with self.assertRaises(frappe.ValidationError):
jv.insert()
def test_clear_depr_schedule(self):
"""Tests if clear_depr_schedule() works as expected."""
@@ -1343,7 +1351,7 @@ class TestDepreciationBasics(AssetSetup):
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active")
asset_depr_schedule_doc.clear_depr_schedule()
asset_depr_schedule_doc.clear()
self.assertEqual(len(asset_depr_schedule_doc.get("depreciation_schedule")), 1)
@@ -1390,15 +1398,15 @@ class TestDepreciationBasics(AssetSetup):
asset.load_from_db()
asset_depr_schedule_doc_1 = get_asset_depr_schedule_doc(asset.name, "Active", "Test Finance Book 1")
asset_depr_schedule_doc_1.clear_depr_schedule()
asset_depr_schedule_doc_1.clear()
self.assertEqual(len(asset_depr_schedule_doc_1.get("depreciation_schedule")), 3)
asset_depr_schedule_doc_2 = get_asset_depr_schedule_doc(asset.name, "Active", "Test Finance Book 2")
asset_depr_schedule_doc_2.clear_depr_schedule()
asset_depr_schedule_doc_2.clear()
self.assertEqual(len(asset_depr_schedule_doc_2.get("depreciation_schedule")), 3)
asset_depr_schedule_doc_3 = get_asset_depr_schedule_doc(asset.name, "Active", "Test Finance Book 3")
asset_depr_schedule_doc_3.clear_depr_schedule()
asset_depr_schedule_doc_3.clear()
self.assertEqual(len(asset_depr_schedule_doc_3.get("depreciation_schedule")), 0)
def test_depreciation_schedules_are_set_up_for_multiple_finance_books(self):
@@ -1448,6 +1456,11 @@ class TestDepreciationBasics(AssetSetup):
submit=1,
)
depr_expense_account = frappe.get_doc("Account", "_Test Depreciations - _TC")
depr_expense_account.root_type = "Expense"
depr_expense_account.parent_account = "Expenses - _TC"
depr_expense_account.save()
post_depreciation_entries(date="2021-01-01")
asset.load_from_db()
@@ -1519,7 +1532,7 @@ class TestDepreciationBasics(AssetSetup):
)
self.assertSequenceEqual(gle, expected_gle)
self.assertEqual(asset.get("value_after_depreciation"), 0)
self.assertEqual(asset.get("value_after_depreciation"), 70000)
def test_expected_value_change(self):
"""
@@ -1616,6 +1629,10 @@ class TestDepreciationBasics(AssetSetup):
frequency_of_depreciation=1,
submit=1,
)
depr_expense_account = frappe.get_doc("Account", "_Test Depreciations - _TC")
depr_expense_account.root_type = "Expense"
depr_expense_account.parent_account = "Expenses - _TC"
depr_expense_account.save()
self.assertEqual(asset.status, "Submitted")
self.assertEqual(asset.get_value_after_depreciation(), 100000)

View File

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

View File

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

View File

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

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
from frappe.tests import IntegrationTestCase, UnitTestCase
from frappe.utils import cstr, flt
from frappe.utils import cstr, date_diff, flt, getdate
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.assets.doctype.asset.depreciation import (
post_depreciation_entries,
)
@@ -13,6 +14,10 @@ from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_sched
get_asset_depr_schedule_doc,
get_depr_schedule,
)
from erpnext.assets.doctype.asset_repair.test_asset_repair import create_asset_repair
from erpnext.assets.doctype.asset_value_adjustment.test_asset_value_adjustment import (
make_asset_value_adjustment,
)
class UnitTestAssetDepreciationSchedule(UnitTestCase):
@@ -41,6 +46,7 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
self.assertRaises(frappe.ValidationError, second_asset_depr_schedule.insert)
def test_daily_prorata_based_depr_on_sl_method(self):
frappe.db.set_single_value("Accounts Settings", "calculate_depr_using_total_days", 0)
asset = create_asset(
calculate_depreciation=1,
depreciation_method="Straight Line",
@@ -178,15 +184,15 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
)
expected_schedules = [
["2024-12-31", 60.92, 284.07],
["2025-03-31", 60.92, 344.99],
["2025-06-30", 60.92, 405.91],
["2025-09-30", 60.92, 466.83],
["2025-12-31", 60.92, 527.75],
["2026-03-31", 60.92, 588.67],
["2026-06-30", 60.92, 649.59],
["2026-09-30", 60.92, 710.51],
["2026-11-01", 20.49, 731.0],
["2024-12-31", 60.98, 284.13],
["2025-03-31", 60.98, 345.11],
["2025-06-30", 60.98, 406.09],
["2025-09-30", 60.98, 467.07],
["2025-12-31", 60.98, 528.05],
["2026-03-31", 60.98, 589.03],
["2026-06-30", 60.98, 650.01],
["2026-09-30", 60.98, 710.99],
["2026-11-01", 20.01, 731.0],
]
schedules = [
[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount]
@@ -273,12 +279,12 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
expected_schedules = [
["2021-03-31", 4383.56, 4383.56],
["2021-06-30", 9535.45, 13919.01],
["2021-09-30", 9640.23, 23559.24],
["2021-12-31", 9640.23, 33199.47],
["2022-03-31", 9430.66, 42630.13],
["2022-06-30", 5721.27, 48351.4],
["2022-08-20", 51648.6, 100000.0],
["2021-06-30", 9972.6, 14356.16],
["2021-09-30", 10082.19, 24438.35],
["2021-12-31", 10082.19, 34520.54],
["2022-03-31", 6458.25, 40978.79],
["2022-06-30", 6530.01, 47508.8],
["2022-08-20", 52491.2, 100000.0],
]
schedules = [
@@ -302,13 +308,13 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
)
expected_schedules = [
["2020-02-29", 1092.90, 1092.90],
["2020-08-31", 19944.01, 21036.91],
["2021-02-28", 19618.83, 40655.74],
["2021-08-31", 11966.4, 52622.14],
["2022-02-28", 11771.3, 64393.44],
["2022-08-31", 7179.84, 71573.28],
["2023-02-20", 28426.72, 100000.0],
["2020-02-29", 1092.9, 1092.9],
["2020-08-31", 20109.29, 21202.19],
["2021-02-28", 15630.03, 36832.22],
["2021-08-31", 15889.09, 52721.31],
["2022-02-28", 9378.02, 62099.33],
["2022-08-31", 9533.46, 71632.79],
["2023-02-20", 28367.21, 100000.0],
]
schedules = [
@@ -377,36 +383,790 @@ class TestAssetDepreciationSchedule(IntegrationTestCase):
self.assertEqual(asset.finance_books[0].total_number_of_booked_depreciations, 14)
def test_schedule_for_wdv_method_for_existing_asset(self):
def test_depreciation_schedule_after_cancelling_asset_repair(self):
asset = create_asset(
item_code="Macbook Pro",
gross_purchase_amount=500,
calculate_depreciation=1,
depreciation_method="Written Down Value",
available_for_use_date="2020-07-17",
is_existing_asset=1,
opening_number_of_booked_depreciations=2,
opening_accumulated_depreciation=11666.67,
depreciation_start_date="2021-04-30",
depreciation_method="Straight Line",
available_for_use_date="2023-01-01",
depreciation_start_date="2023-01-31",
frequency_of_depreciation=1,
total_number_of_depreciations=12,
frequency_of_depreciation=3,
gross_purchase_amount=50000,
rate_of_depreciation=40,
submit=1,
)
self.assertEqual(asset.status, "Draft")
expected_schedules = [
["2021-04-30", 3833.33, 15500.0],
["2021-07-31", 3833.33, 19333.33],
["2021-10-31", 3833.33, 23166.66],
["2022-01-31", 3833.33, 26999.99],
["2022-04-30", 2300.0, 29299.99],
["2022-07-31", 2300.0, 31599.99],
["2022-10-31", 2300.0, 33899.99],
["2023-01-31", 2300.0, 36199.99],
["2023-04-30", 1380.0, 37579.99],
["2023-07-31", 12420.01, 50000.0],
expected_depreciation_before_repair = [
["2023-01-31", 41.67, 41.67],
["2023-02-28", 41.67, 83.34],
["2023-03-31", 41.67, 125.01],
["2023-04-30", 41.67, 166.68],
["2023-05-31", 41.67, 208.35],
["2023-06-30", 41.67, 250.02],
["2023-07-31", 41.67, 291.69],
["2023-08-31", 41.67, 333.36],
["2023-09-30", 41.67, 375.03],
["2023-10-31", 41.67, 416.7],
["2023-11-30", 41.67, 458.37],
["2023-12-31", 41.63, 500.0],
]
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in get_depr_schedule(asset.name, "Active")
]
self.assertEqual(schedules, expected_depreciation_before_repair)
self.assertEqual(asset.finance_books[0].value_after_depreciation, 500)
asset_repair = create_asset_repair(
asset=asset,
capitalize_repair_cost=1,
item="_Test Non Stock Item",
failure_date="2023-04-01",
pi_repair_cost1=60,
pi_repair_cost2=40,
increase_in_asset_life=0,
submit=1,
)
self.assertEqual(asset_repair.total_repair_cost, 100)
expected_depreciation_after_repair = [
["2023-01-31", 50.0, 50.0],
["2023-02-28", 50.0, 100.0],
["2023-03-31", 50.0, 150.0],
["2023-04-30", 50.0, 200.0],
["2023-05-31", 50.0, 250.0],
["2023-06-30", 50.0, 300.0],
["2023-07-31", 50.0, 350.0],
["2023-08-31", 50.0, 400.0],
["2023-09-30", 50.0, 450.0],
["2023-10-31", 50.0, 500.0],
["2023-11-30", 50.0, 550.0],
["2023-12-31", 50.0, 600.0],
]
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in get_depr_schedule(asset.name, "Active")
]
self.assertEqual(schedules, expected_depreciation_after_repair)
asset.reload()
self.assertEqual(asset.finance_books[0].value_after_depreciation, 600)
asset_repair.cancel()
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in get_depr_schedule(asset.name, "Active")
]
self.assertEqual(schedules, expected_depreciation_before_repair)
asset.reload()
self.assertEqual(asset.finance_books[0].value_after_depreciation, 500)
def test_depreciation_schedule_after_cancelling_asset_repair_for_6_months_frequency(self):
asset = create_asset(
item_code="Macbook Pro",
gross_purchase_amount=500,
calculate_depreciation=1,
depreciation_method="Straight Line",
available_for_use_date="2023-01-01",
depreciation_start_date="2023-06-30",
frequency_of_depreciation=6,
total_number_of_depreciations=4,
submit=1,
)
expected_depreciation_before_repair = [
["2023-06-30", 125.0, 125.0],
["2023-12-31", 125.0, 250.0],
["2024-06-30", 125.0, 375.0],
["2024-12-31", 125.0, 500.0],
]
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in get_depr_schedule(asset.name, "Active")
]
self.assertEqual(schedules, expected_depreciation_before_repair)
asset_repair = create_asset_repair(
asset=asset,
capitalize_repair_cost=1,
item="_Test Non Stock Item",
failure_date="2023-04-01",
pi_repair_cost1=60,
pi_repair_cost2=40,
increase_in_asset_life=0,
submit=1,
)
self.assertEqual(asset_repair.total_repair_cost, 100)
expected_depreciation_after_repair = [
["2023-06-30", 150.0, 150.0],
["2023-12-31", 150.0, 300.0],
["2024-06-30", 150.0, 450.0],
["2024-12-31", 150.0, 600.0],
]
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in get_depr_schedule(asset.name, "Active")
]
self.assertEqual(schedules, expected_depreciation_after_repair)
asset.reload()
self.assertEqual(asset.finance_books[0].value_after_depreciation, 600)
asset_repair.cancel()
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in get_depr_schedule(asset.name, "Active")
]
self.assertEqual(schedules, expected_depreciation_before_repair)
asset.reload()
self.assertEqual(asset.finance_books[0].value_after_depreciation, 500)
def test_depreciation_schedule_after_cancelling_asset_repair_for_existing_asset(self):
asset = create_asset(
item_code="Macbook Pro",
gross_purchase_amount=500,
calculate_depreciation=1,
depreciation_method="Straight Line",
available_for_use_date="2023-01-15",
depreciation_start_date="2023-03-31",
frequency_of_depreciation=1,
total_number_of_depreciations=12,
is_existing_asset=1,
opening_accumulated_depreciation=64.52,
opening_number_of_booked_depreciations=2,
submit=1,
)
expected_depreciation_before_repair = [
["2023-03-31", 41.39, 105.91],
["2023-04-30", 41.39, 147.3],
["2023-05-31", 41.39, 188.69],
["2023-06-30", 41.39, 230.08],
["2023-07-31", 41.39, 271.47],
["2023-08-31", 41.39, 312.86],
["2023-09-30", 41.39, 354.25],
["2023-10-31", 41.39, 395.64],
["2023-11-30", 41.39, 437.03],
["2023-12-31", 41.39, 478.42],
["2024-01-15", 21.58, 500.0],
]
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in get_depr_schedule(asset.name, "Active")
]
self.assertEqual(schedules, expected_depreciation_before_repair)
asset_repair = create_asset_repair(
asset=asset,
capitalize_repair_cost=1,
item="_Test Non Stock Item",
failure_date="2023-04-01",
pi_repair_cost1=60,
pi_repair_cost2=40,
increase_in_asset_life=0,
submit=1,
)
self.assertEqual(asset_repair.total_repair_cost, 100)
expected_depreciation_after_repair = [
["2023-03-31", 50.9, 115.42],
["2023-04-30", 50.9, 166.32],
["2023-05-31", 50.9, 217.22],
["2023-06-30", 50.9, 268.12],
["2023-07-31", 50.9, 319.02],
["2023-08-31", 50.9, 369.92],
["2023-09-30", 50.9, 420.82],
["2023-10-31", 50.9, 471.72],
["2023-11-30", 50.9, 522.62],
["2023-12-31", 50.9, 573.52],
["2024-01-15", 26.48, 600.0],
]
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in get_depr_schedule(asset.name, "Active")
]
self.assertEqual(schedules, expected_depreciation_after_repair)
asset_repair.cancel()
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in get_depr_schedule(asset.name, "Active")
]
self.assertEqual(schedules, expected_depreciation_before_repair)
asset.reload()
self.assertEqual(asset.finance_books[0].value_after_depreciation, 435.48)
def test_wdv_depreciation_schedule_after_cancelling_asset_repair(self):
asset = create_asset(
item_code="Macbook Pro",
gross_purchase_amount=500,
calculate_depreciation=1,
depreciation_method="Written Down Value",
available_for_use_date="2023-04-01",
depreciation_start_date="2023-12-31",
frequency_of_depreciation=12,
total_number_of_depreciations=4,
rate_of_depreciation=40,
submit=1,
)
expected_depreciation_before_repair = [
["2023-12-31", 150.68, 150.68],
["2024-12-31", 139.73, 290.41],
["2025-12-31", 83.84, 374.25],
["2026-12-31", 50.3, 424.55],
["2027-04-01", 75.45, 500.0],
]
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in get_depr_schedule(asset.name, "Active")
]
self.assertEqual(schedules, expected_depreciation_before_repair)
asset_repair = create_asset_repair(
asset=asset,
capitalize_repair_cost=1,
item="_Test Non Stock Item",
failure_date="2024-01-01",
pi_repair_cost1=60,
pi_repair_cost2=40,
increase_in_asset_life=0,
submit=1,
)
expected_depreciation_after_repair = [
["2023-12-31", 180.82, 180.82],
["2024-12-31", 167.67, 348.49],
["2025-12-31", 100.6, 449.09],
["2026-12-31", 60.36, 509.45],
["2027-04-01", 90.55, 600.0],
]
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in get_depr_schedule(asset.name, "Active")
]
self.assertEqual(schedules, expected_depreciation_after_repair)
asset_repair.cancel()
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in get_depr_schedule(asset.name, "Active")
]
self.assertEqual(schedules, expected_depreciation_before_repair)
def test_daily_prorata_based_depreciation_schedule_after_cancelling_asset_repair_for(self):
asset = create_asset(
item_code="Macbook Pro",
gross_purchase_amount=500,
calculate_depreciation=1,
depreciation_method="Straight Line",
available_for_use_date="2023-01-01",
depreciation_start_date="2023-01-31",
daily_prorata_based=1,
frequency_of_depreciation=1,
total_number_of_depreciations=12,
submit=1,
)
expected_depreciation_before_repair = [
["2023-01-31", 42.47, 42.47],
["2023-02-28", 38.36, 80.83],
["2023-03-31", 42.47, 123.3],
["2023-04-30", 41.1, 164.4],
["2023-05-31", 42.47, 206.87],
["2023-06-30", 41.1, 247.97],
["2023-07-31", 42.47, 290.44],
["2023-08-31", 42.47, 332.91],
["2023-09-30", 41.1, 374.01],
["2023-10-31", 42.47, 416.48],
["2023-11-30", 41.1, 457.58],
["2023-12-31", 42.42, 500.0],
]
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in get_depr_schedule(asset.name, "Active")
]
self.assertEqual(schedules, expected_depreciation_before_repair)
asset_repair = create_asset_repair(
asset=asset,
capitalize_repair_cost=1,
item="_Test Non Stock Item",
failure_date="2023-04-01",
pi_repair_cost1=60,
pi_repair_cost2=40,
increase_in_asset_life=0,
submit=1,
)
self.assertEqual(asset_repair.total_repair_cost, 100)
expected_depreciation_after_repair = [
["2023-01-31", 50.96, 50.96],
["2023-02-28", 46.03, 96.99],
["2023-03-31", 50.96, 147.95],
["2023-04-30", 49.32, 197.27],
["2023-05-31", 50.96, 248.23],
["2023-06-30", 49.32, 297.55],
["2023-07-31", 50.96, 348.51],
["2023-08-31", 50.96, 399.47],
["2023-09-30", 49.32, 448.79],
["2023-10-31", 50.96, 499.75],
["2023-11-30", 49.32, 549.07],
["2023-12-31", 50.93, 600.0],
]
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in get_depr_schedule(asset.name, "Active")
]
self.assertEqual(schedules, expected_depreciation_after_repair)
asset.reload()
self.assertEqual(asset.finance_books[0].value_after_depreciation, 600)
asset_repair.cancel()
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in get_depr_schedule(asset.name, "Active")
]
self.assertEqual(schedules, expected_depreciation_before_repair)
asset.reload()
self.assertEqual(asset.finance_books[0].value_after_depreciation, 500)
def test_depreciation_schedule_after_cancelling_asset_value_adjustent(self):
asset = create_asset(
item_code="Macbook Pro",
gross_purchase_amount=1000,
calculate_depreciation=1,
depreciation_method="Straight Line",
available_for_use_date="2023-01-01",
depreciation_start_date="2023-01-31",
frequency_of_depreciation=1,
total_number_of_depreciations=12,
submit=1,
)
expected_depreciation_before_adjustment = [
["2023-01-31", 83.33, 83.33],
["2023-02-28", 83.33, 166.66],
["2023-03-31", 83.33, 249.99],
["2023-04-30", 83.33, 333.32],
["2023-05-31", 83.33, 416.65],
["2023-06-30", 83.33, 499.98],
["2023-07-31", 83.33, 583.31],
["2023-08-31", 83.33, 666.64],
["2023-09-30", 83.33, 749.97],
["2023-10-31", 83.33, 833.3],
["2023-11-30", 83.33, 916.63],
["2023-12-31", 83.37, 1000.0],
]
schedules = [
[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount]
for d in get_depr_schedule(asset.name, "Draft")
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in get_depr_schedule(asset.name, "Active")
]
self.assertEqual(schedules, expected_schedules)
self.assertEqual(schedules, expected_depreciation_before_adjustment)
current_asset_value = asset.finance_books[0].value_after_depreciation
asset_value_adjustment = make_asset_value_adjustment(
asset=asset,
date="2023-04-01",
current_asset_value=current_asset_value,
new_asset_value=1200,
)
asset_value_adjustment.submit()
expected_depreciation_after_adjustment = [
["2023-01-31", 100.0, 100.0],
["2023-02-28", 100.0, 200.0],
["2023-03-31", 100.0, 300.0],
["2023-04-30", 100.0, 400.0],
["2023-05-31", 100.0, 500.0],
["2023-06-30", 100.0, 600.0],
["2023-07-31", 100.0, 700.0],
["2023-08-31", 100.0, 800.0],
["2023-09-30", 100.0, 900.0],
["2023-10-31", 100.0, 1000.0],
["2023-11-30", 100.0, 1100.0],
["2023-12-31", 100.0, 1200.0],
]
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in get_depr_schedule(asset.name, "Active")
]
self.assertEqual(schedules, expected_depreciation_after_adjustment)
asset_value_adjustment.cancel()
asset.reload()
self.assertEqual(asset.finance_books[0].value_after_depreciation, 1000)
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in get_depr_schedule(asset.name, "Active")
]
self.assertEqual(schedules, expected_depreciation_before_adjustment)
def test_depreciation_on_return_of_sold_asset(self):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
create_asset_data()
asset = create_asset(item_code="Macbook Pro", calculate_depreciation=1, submit=1)
post_depreciation_entries(getdate("2021-09-30"))
si = create_sales_invoice(
item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000, posting_date=getdate("2021-09-30")
)
return_si = make_return_doc("Sales Invoice", si.name)
return_si.submit()
asset.load_from_db()
expected_values = [
["2020-06-30", 1366.12, 1366.12, True],
["2021-06-30", 20000.0, 21366.12, True],
["2022-06-30", 20000.95, 41367.07, False],
["2023-06-30", 20000.95, 61368.02, False],
["2024-06-30", 20000.95, 81368.97, False],
["2025-06-06", 18631.03, 100000.0, False],
]
for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")):
self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
self.assertEqual(expected_values[i][1], schedule.depreciation_amount)
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
self.assertEqual(schedule.journal_entry, schedule.journal_entry)
def test_depreciation_schedule_after_cancelling_asset_value_adjustent_for_existing_asset(self):
asset = create_asset(
item_code="Macbook Pro",
gross_purchase_amount=500,
calculate_depreciation=1,
depreciation_method="Straight Line",
available_for_use_date="2023-01-15",
depreciation_start_date="2023-03-31",
frequency_of_depreciation=1,
total_number_of_depreciations=12,
is_existing_asset=1,
opening_accumulated_depreciation=64.52,
opening_number_of_booked_depreciations=2,
submit=1,
)
expected_depreciation_before_adjustment = [
["2023-03-31", 41.39, 105.91],
["2023-04-30", 41.39, 147.3],
["2023-05-31", 41.39, 188.69],
["2023-06-30", 41.39, 230.08],
["2023-07-31", 41.39, 271.47],
["2023-08-31", 41.39, 312.86],
["2023-09-30", 41.39, 354.25],
["2023-10-31", 41.39, 395.64],
["2023-11-30", 41.39, 437.03],
["2023-12-31", 41.39, 478.42],
["2024-01-15", 21.58, 500.0],
]
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in get_depr_schedule(asset.name, "Active")
]
self.assertEqual(schedules, expected_depreciation_before_adjustment)
current_asset_value = asset.finance_books[0].value_after_depreciation
asset_value_adjustment = make_asset_value_adjustment(
asset=asset,
date="2023-04-01",
current_asset_value=current_asset_value,
new_asset_value=600,
)
asset_value_adjustment.submit()
expected_depreciation_after_adjustment = [
["2023-03-31", 57.03, 121.55],
["2023-04-30", 57.03, 178.58],
["2023-05-31", 57.03, 235.61],
["2023-06-30", 57.03, 292.64],
["2023-07-31", 57.03, 349.67],
["2023-08-31", 57.03, 406.7],
["2023-09-30", 57.03, 463.73],
["2023-10-31", 57.03, 520.76],
["2023-11-30", 57.03, 577.79],
["2023-12-31", 57.03, 634.82],
["2024-01-15", 29.7, 664.52],
]
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in get_depr_schedule(asset.name, "Active")
]
self.assertEqual(schedules, expected_depreciation_after_adjustment)
asset_value_adjustment.cancel()
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in get_depr_schedule(asset.name, "Active")
]
self.assertEqual(schedules, expected_depreciation_before_adjustment)
def test_depreciation_schedule_for_parallel_adjustment_and_repair(self):
asset = create_asset(
item_code="Macbook Pro",
gross_purchase_amount=600,
calculate_depreciation=1,
depreciation_method="Straight Line",
available_for_use_date="2021-01-01",
depreciation_start_date="2021-12-31",
frequency_of_depreciation=12,
total_number_of_depreciations=3,
is_existing_asset=1,
submit=1,
)
post_depreciation_entries(date="2021-12-31")
asset.reload()
expected_depreciation_before_adjustment = [
["2021-12-31", 200, 200],
["2022-12-31", 200, 400],
["2023-12-31", 200, 600],
]
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in get_depr_schedule(asset.name, "Active")
]
self.assertEqual(schedules, expected_depreciation_before_adjustment)
current_asset_value = asset.finance_books[0].value_after_depreciation
asset_value_adjustment = make_asset_value_adjustment(
asset=asset,
date="2022-01-15",
current_asset_value=current_asset_value,
new_asset_value=500,
)
asset_value_adjustment.submit()
expected_depreciation_after_adjustment = [
["2021-12-31", 200, 200],
["2022-12-31", 250, 450],
["2023-12-31", 250, 700],
]
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in get_depr_schedule(asset.name, "Active")
]
self.assertEqual(schedules, expected_depreciation_after_adjustment)
asset_repair = create_asset_repair(
asset=asset,
capitalize_repair_cost=1,
item="_Test Non Stock Item",
failure_date="2022-01-20",
pi_repair_cost1=60,
pi_repair_cost2=40,
increase_in_asset_life=0,
submit=1,
)
self.assertEqual(asset_repair.total_repair_cost, 100)
expected_depreciation_after_repair = [
["2021-12-31", 200, 200],
["2022-12-31", 300, 500],
["2023-12-31", 300, 800],
]
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in get_depr_schedule(asset.name, "Active")
]
self.assertEqual(schedules, expected_depreciation_after_repair)
asset.reload()
asset_value_adjustment.cancel()
expected_depreciation_after_cancelling_adjustment = [
["2021-12-31", 200, 200],
["2022-12-31", 250, 450],
["2023-12-31", 250, 700],
]
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in get_depr_schedule(asset.name, "Active")
]
self.assertEqual(schedules, expected_depreciation_after_cancelling_adjustment)
def test_depreciation_schedule_after_sale_of_asset(self):
asset = create_asset(
item_code="Macbook Pro",
gross_purchase_amount=600,
calculate_depreciation=1,
depreciation_method="Straight Line",
available_for_use_date="2021-01-01",
depreciation_start_date="2021-12-31",
frequency_of_depreciation=12,
total_number_of_depreciations=3,
is_existing_asset=1,
submit=1,
)
post_depreciation_entries(date="2021-12-31")
asset.reload()
expected_depreciation_before_adjustment = [
["2021-12-31", 200, 200],
["2022-12-31", 200, 400],
["2023-12-31", 200, 600],
]
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in get_depr_schedule(asset.name, "Active")
]
self.assertEqual(schedules, expected_depreciation_before_adjustment)
current_asset_value = asset.finance_books[0].value_after_depreciation
asset_value_adjustment = make_asset_value_adjustment(
asset=asset,
date="2022-01-15",
current_asset_value=current_asset_value,
new_asset_value=500,
)
asset_value_adjustment.submit()
expected_depreciation_after_adjustment = [
["2021-12-31", 200, 200],
["2022-12-31", 250, 450],
["2023-12-31", 250, 700],
]
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in get_depr_schedule(asset.name, "Active")
]
self.assertEqual(schedules, expected_depreciation_after_adjustment)
si = create_sales_invoice(
item_code="Macbook Pro", asset=asset.name, qty=1, rate=300, posting_date=getdate("2022-04-01")
)
asset.load_from_db()
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
expected_depreciation_after_sale = [
["2021-12-31", 200.0, 200.0],
["2022-04-01", 62.33, 262.33],
]
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in get_depr_schedule(asset.name, "Active")
]
self.assertEqual(schedules, expected_depreciation_after_sale)
si.cancel()
asset.reload()
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated")
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in get_depr_schedule(asset.name, "Active")
]
self.assertEqual(schedules, expected_depreciation_after_adjustment)
def test_depreciation_schedule_after_sale_of_asset_wdv_method(self):
asset = create_asset(
item_code="Macbook Pro",
gross_purchase_amount=500,
calculate_depreciation=1,
depreciation_method="Written Down Value",
available_for_use_date="2021-01-01",
depreciation_start_date="2021-12-31",
rate_of_depreciation=50,
frequency_of_depreciation=12,
total_number_of_depreciations=3,
is_existing_asset=1,
submit=1,
)
post_depreciation_entries(date="2021-12-31")
asset.reload()
expected_depreciation_before_repair = [
["2021-12-31", 250.0, 250.0],
["2022-12-31", 125.0, 375.0],
["2023-12-31", 125.0, 500.0],
]
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in get_depr_schedule(asset.name, "Active")
]
self.assertEqual(schedules, expected_depreciation_before_repair)
create_asset_repair(
asset=asset,
capitalize_repair_cost=1,
item="_Test Non Stock Item",
failure_date="2022-03-01",
pi_repair_cost1=60,
pi_repair_cost2=40,
increase_in_asset_life=0,
submit=1,
)
expected_depreciation_after_repair = [
["2021-12-31", 250.0, 250.0],
["2022-12-31", 175.0, 425.0],
["2023-12-31", 175.0, 600.0],
]
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in get_depr_schedule(asset.name, "Active")
]
self.assertEqual(schedules, expected_depreciation_after_repair)
si = create_sales_invoice(
item_code="Macbook Pro", asset=asset.name, qty=1, rate=300, posting_date=getdate("2022-04-01")
)
asset.load_from_db()
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
expected_depreciation_after_sale = [
["2021-12-31", 250.0, 250.0],
["2022-04-01", 43.63, 293.63],
]
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in get_depr_schedule(asset.name, "Active")
]
self.assertEqual(schedules, expected_depreciation_after_sale)
si.cancel()
asset.reload()
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated")
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in get_depr_schedule(asset.name, "Active")
]
self.assertEqual(schedules, expected_depreciation_after_repair)

View File

@@ -9,6 +9,7 @@
"depreciation_method",
"frequency_of_depreciation",
"total_number_of_depreciations",
"increase_in_asset_life",
"depreciation_start_date",
"column_break_5",
"salvage_value_percentage",
@@ -89,7 +90,8 @@
"fieldname": "rate_of_depreciation",
"fieldtype": "Percent",
"label": "Rate of Depreciation (%)",
"mandatory_depends_on": "eval:doc.depreciation_method == 'Written Down Value'"
"mandatory_depends_on": "eval:doc.depreciation_method == 'Written Down Value'",
"no_copy": 1
},
{
"fieldname": "salvage_value_percentage",
@@ -115,6 +117,7 @@
"fieldname": "total_number_of_booked_depreciations",
"fieldtype": "Int",
"label": "Total Number of Booked Depreciations ",
"no_copy": 1,
"read_only": 1
},
{
@@ -124,12 +127,20 @@
{
"fieldname": "column_break_sigk",
"fieldtype": "Column Break"
},
{
"description": "via Asset Repair",
"fieldname": "increase_in_asset_life",
"fieldtype": "Int",
"label": "Increase In Asset Life (Months)",
"no_copy": 1,
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-12-13 12:11:03.743209",
"modified": "2025-01-06 17:14:51.836803",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Finance Book",

View File

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

View File

@@ -34,7 +34,16 @@ frappe.ui.form.on("Asset Repair", {
query: "erpnext.assets.doctype.asset_repair.asset_repair.get_purchase_invoice",
filters: {
company: frm.doc.company,
docstatus: 1,
},
};
});
frm.set_query("expense_account", "invoices", function (doc, cdt, cdn) {
let row = locals[cdt][cdn];
return {
query: "erpnext.assets.doctype.asset_repair.asset_repair.get_expense_accounts",
filters: {
purchase_invoice: row.purchase_invoice,
},
};
});
@@ -59,16 +68,6 @@ frappe.ui.form.on("Asset Repair", {
},
};
});
frm.set_query("expense_account", "invoices", function () {
return {
filters: {
company: frm.doc.company,
is_group: ["=", 0],
report_type: ["=", "Profit and Loss"],
},
};
});
},
refresh: function (frm) {

View File

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

View File

@@ -4,7 +4,7 @@
import frappe
from frappe import _
from frappe.query_builder import DocType
from frappe.utils import add_months, cint, flt, get_link_to_form, getdate, time_diff_in_hours
from frappe.utils import cint, flt, get_link_to_form, getdate, time_diff_in_hours
import erpnext
from erpnext.accounts.general_ledger import make_gl_entries
@@ -12,7 +12,7 @@ from erpnext.assets.doctype.asset.asset import get_asset_account
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_depr_schedule,
make_new_active_asset_depr_schedules_and_cancel_current_ones,
reschedule_depreciation,
)
from erpnext.controllers.accounts_controller import AccountsController
@@ -40,6 +40,7 @@ class AssetRepair(AccountsController):
capitalize_repair_cost: DF.Check
company: DF.Link | None
completion_date: DF.Datetime | None
consumed_items_cost: DF.Currency
cost_center: DF.Link | None
description: DF.LongText | None
downtime: DF.Data | None
@@ -50,7 +51,6 @@ class AssetRepair(AccountsController):
project: DF.Link | None
repair_cost: DF.Currency
repair_status: DF.Literal["Pending", "Completed", "Cancelled"]
stock_consumption: DF.Check
stock_items: DF.Table[AssetRepairConsumedItem]
total_repair_cost: DF.Currency
# end: auto-generated types
@@ -58,16 +58,12 @@ class AssetRepair(AccountsController):
def validate(self):
self.asset_doc = frappe.get_doc("Asset", self.asset)
self.validate_dates()
self.validate_purchase_invoice()
self.validate_purchase_invoice_repair_cost()
self.validate_purchase_invoice_expense_account()
self.validate_purchase_invoices()
self.update_status()
if self.get("stock_items"):
self.set_stock_items_cost()
self.calculate_consumed_items_cost()
self.calculate_repair_cost()
self.calculate_total_repair_cost()
self.check_repair_status()
def validate_dates(self):
if self.completion_date and (self.failure_date > self.completion_date):
@@ -75,36 +71,58 @@ class AssetRepair(AccountsController):
_("Completion Date can not be before Failure Date. Please adjust the dates accordingly.")
)
def validate_purchase_invoice(self):
query = expense_item_pi_query(self.company)
purchase_invoice_list = [item[0] for item in query.run()]
for pi in self.invoices:
if pi.purchase_invoice not in purchase_invoice_list:
frappe.throw(_("Expense item not present in Purchase Invoice"))
def validate_purchase_invoices(self):
for d in self.invoices:
invoice_items = self.get_invoice_items(d.purchase_invoice)
self.validate_service_purchase_invoice(d.purchase_invoice, invoice_items)
self.validate_expense_account(d, invoice_items)
self.validate_purchase_invoice_repair_cost(d, invoice_items)
def validate_purchase_invoice_repair_cost(self):
for pi in self.invoices:
if flt(pi.repair_cost) > frappe.db.get_value(
"Purchase Invoice", pi.purchase_invoice, "base_net_total"
):
frappe.throw(_("Repair cost cannot be greater than purchase invoice base net total"))
def get_invoice_items(self, pi):
invoice_items = frappe.get_all(
"Purchase Invoice Item",
filters={"parent": pi},
fields=["item_code", "expense_account", "base_net_amount"],
)
def validate_purchase_invoice_expense_account(self):
for pi in self.invoices:
if pi.expense_account not in frappe.db.get_all(
"Purchase Invoice Item", {"parent": pi.purchase_invoice}, pluck="expense_account"
):
frappe.throw(
_("Expense account not present in Purchase Invoice {0}").format(
get_link_to_form("Purchase Invoice", pi.purchase_invoice)
)
return invoice_items
def validate_service_purchase_invoice(self, purchase_invoice, invoice_items):
service_item_exists = False
for item in invoice_items:
if frappe.db.get_value("Item", item.item_code, "is_stock_item") == 0:
service_item_exists = True
break
if not service_item_exists:
frappe.throw(
_("Service item not present in Purchase Invoice {0}").format(
get_link_to_form("Purchase Invoice", purchase_invoice)
)
)
def validate_expense_account(self, row, invoice_items):
pi_expense_accounts = set([item.expense_account for item in invoice_items])
if row.expense_account not in pi_expense_accounts:
frappe.throw(
_("Expense account {0} not present in Purchase Invoice {1}").format(
row.expense_account, get_link_to_form("Purchase Invoice", row.purchase_invoice)
)
)
def validate_purchase_invoice_repair_cost(self, row, invoice_items):
pi_net_total = sum([flt(item.base_net_amount) for item in invoice_items])
if flt(row.repair_cost) > pi_net_total:
frappe.throw(
_("Repair cost cannot be greater than purchase invoice base net total {0}").format(
pi_net_total
)
)
def update_status(self):
if self.repair_status == "Pending" and self.asset_doc.status != "Out of Order":
frappe.db.set_value("Asset", self.asset, "status", "Out of Order")
add_asset_activity(
self.asset,
self.add_asset_activity(
_("Asset out of order due to Asset Repair {0}").format(
get_link_to_form("Asset Repair", self.name)
),
@@ -112,147 +130,78 @@ class AssetRepair(AccountsController):
else:
self.asset_doc.set_status()
def set_stock_items_cost(self):
def calculate_consumed_items_cost(self):
consumed_items_cost = 0.0
for item in self.get("stock_items"):
item.total_value = flt(item.valuation_rate) * flt(item.consumed_quantity)
consumed_items_cost += item.total_value
self.consumed_items_cost = consumed_items_cost
def calculate_repair_cost(self):
self.repair_cost = sum(flt(pi.repair_cost) for pi in self.invoices)
def calculate_total_repair_cost(self):
self.total_repair_cost = flt(self.repair_cost)
self.total_repair_cost = flt(self.repair_cost) + flt(self.consumed_items_cost)
total_value_of_stock_consumed = self.get_total_value_of_stock_consumed()
self.total_repair_cost += total_value_of_stock_consumed
def on_submit(self):
self.decrease_stock_quantity()
def before_submit(self):
self.check_repair_status()
if self.get("capitalize_repair_cost"):
self.update_asset_value()
self.set_increase_in_asset_life()
self.asset_doc.flags.increase_in_asset_value_due_to_repair = False
depreciation_note = self.get_depreciation_note()
reschedule_depreciation(self.asset_doc, depreciation_note)
self.add_asset_activity()
if self.get("stock_consumption") or self.get("capitalize_repair_cost"):
self.asset_doc.flags.increase_in_asset_value_due_to_repair = True
self.make_gl_entries()
self.increase_asset_value()
total_repair_cost = self.get_total_value_of_stock_consumed()
if self.capitalize_repair_cost:
total_repair_cost += self.repair_cost
self.asset_doc.total_asset_cost += total_repair_cost
self.asset_doc.additional_asset_cost += total_repair_cost
if self.get("stock_consumption"):
self.check_for_stock_items_and_warehouse()
self.decrease_stock_quantity()
if self.get("capitalize_repair_cost"):
self.make_gl_entries()
if self.asset_doc.calculate_depreciation and self.increase_in_asset_life:
self.modify_depreciation_schedule()
notes = _(
"This schedule was created when Asset {0} was repaired through Asset Repair {1}."
).format(
get_link_to_form(self.asset_doc.doctype, self.asset_doc.name),
get_link_to_form(self.doctype, self.name),
)
self.asset_doc.flags.ignore_validate_update_after_submit = True
make_new_active_asset_depr_schedules_and_cancel_current_ones(
self.asset_doc, notes, ignore_booked_entry=True
)
self.asset_doc.save()
add_asset_activity(
self.asset,
_("Asset updated after completion of Asset Repair {0}").format(
get_link_to_form("Asset Repair", self.name)
),
)
def before_cancel(self):
def on_cancel(self):
self.asset_doc = frappe.get_doc("Asset", self.asset)
if self.get("capitalize_repair_cost"):
self.update_asset_value()
self.make_gl_entries(cancel=True)
self.set_increase_in_asset_life()
self.asset_doc.flags.increase_in_asset_value_due_to_repair = False
if self.get("stock_consumption") or self.get("capitalize_repair_cost"):
self.asset_doc.flags.increase_in_asset_value_due_to_repair = True
self.decrease_asset_value()
total_repair_cost = self.get_total_value_of_stock_consumed()
if self.capitalize_repair_cost:
total_repair_cost += self.repair_cost
self.asset_doc.total_asset_cost -= total_repair_cost
self.asset_doc.additional_asset_cost -= total_repair_cost
if self.get("capitalize_repair_cost"):
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
self.make_gl_entries(cancel=True)
if self.asset_doc.calculate_depreciation and self.increase_in_asset_life:
self.revert_depreciation_schedule_on_cancellation()
notes = _(
"This schedule was created when Asset {0}'s Asset Repair {1} was cancelled."
).format(
get_link_to_form(self.asset_doc.doctype, self.asset_doc.name),
get_link_to_form(self.doctype, self.name),
)
self.asset_doc.flags.ignore_validate_update_after_submit = True
make_new_active_asset_depr_schedules_and_cancel_current_ones(
self.asset_doc, notes, ignore_booked_entry=True
)
self.asset_doc.save()
add_asset_activity(
self.asset,
_("Asset updated after cancellation of Asset Repair {0}").format(
get_link_to_form("Asset Repair", self.name)
),
)
depreciation_note = self.get_depreciation_note()
reschedule_depreciation(self.asset_doc, depreciation_note)
self.add_asset_activity()
def after_delete(self):
frappe.get_doc("Asset", self.asset).set_status()
def check_repair_status(self):
if self.repair_status == "Pending":
if self.repair_status == "Pending" and self.docstatus == 1:
frappe.throw(_("Please update Repair Status."))
def check_for_stock_items_and_warehouse(self):
if not self.get("stock_items"):
frappe.throw(_("Please enter Stock Items consumed during the Repair."), title=_("Missing Items"))
def update_asset_value(self):
total_repair_cost = self.total_repair_cost if self.docstatus == 1 else -1 * self.total_repair_cost
def increase_asset_value(self):
total_value_of_stock_consumed = self.get_total_value_of_stock_consumed()
self.asset_doc.total_asset_cost += flt(total_repair_cost)
self.asset_doc.additional_asset_cost += flt(total_repair_cost)
if self.asset_doc.calculate_depreciation:
for row in self.asset_doc.finance_books:
row.value_after_depreciation += total_value_of_stock_consumed
row.value_after_depreciation += flt(total_repair_cost)
if self.capitalize_repair_cost:
row.value_after_depreciation += self.repair_cost
def decrease_asset_value(self):
total_value_of_stock_consumed = self.get_total_value_of_stock_consumed()
if self.asset_doc.calculate_depreciation:
for row in self.asset_doc.finance_books:
row.value_after_depreciation -= total_value_of_stock_consumed
if self.capitalize_repair_cost:
row.value_after_depreciation -= self.repair_cost
self.asset_doc.flags.ignore_validate_update_after_submit = True
self.asset_doc.save()
def get_total_value_of_stock_consumed(self):
total_value_of_stock_consumed = 0
if self.get("stock_consumption"):
for item in self.get("stock_items"):
total_value_of_stock_consumed += item.total_value
return total_value_of_stock_consumed
return sum([flt(item.total_value) for item in self.get("stock_items")])
def decrease_stock_quantity(self):
if not self.get("stock_items"):
return
stock_entry = frappe.get_doc(
{"doctype": "Stock Entry", "stock_entry_type": "Material Issue", "company": self.company}
{
"doctype": "Stock Entry",
"stock_entry_type": "Material Issue",
"company": self.company,
"asset_repair": self.name,
}
)
stock_entry.asset_repair = self.name
for stock_item in self.get("stock_items"):
self.validate_serial_no(stock_item)
@@ -278,7 +227,7 @@ class AssetRepair(AccountsController):
"Item", stock_item.item_code, "has_serial_no"
):
msg = f"Serial No Bundle is mandatory for Item {stock_item.item_code}"
frappe.throw(msg, title=_("Missing Serial No Bundle"))
frappe.throw(_(msg), title=_("Missing Serial No Bundle"))
if stock_item.serial_and_batch_bundle:
values_to_update = {
@@ -291,6 +240,9 @@ class AssetRepair(AccountsController):
)
def make_gl_entries(self, cancel=False):
if cancel:
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
if flt(self.total_repair_cost) > 0:
gl_entries = self.get_gl_entries()
make_gl_entries(gl_entries, cancel)
@@ -348,7 +300,7 @@ class AssetRepair(AccountsController):
)
def get_gl_entries_for_consumed_items(self, gl_entries, fixed_asset_account):
if not (self.get("stock_consumption") and self.get("stock_items")):
if not self.get("stock_items"):
return
# creating GL Entries for each row in Stock Items based on the Stock Entry created for it
@@ -400,72 +352,28 @@ class AssetRepair(AccountsController):
)
)
def modify_depreciation_schedule(self):
for row in self.asset_doc.finance_books:
row.total_number_of_depreciations += self.increase_in_asset_life / row.frequency_of_depreciation
def set_increase_in_asset_life(self):
if self.asset_doc.calculate_depreciation and cint(self.increase_in_asset_life) > 0:
for row in self.asset_doc.finance_books:
row.increase_in_asset_life = cint(row.increase_in_asset_life) + (
cint(self.increase_in_asset_life) * (1 if self.docstatus == 1 else -1)
)
row.db_update()
self.asset_doc.flags.increase_in_asset_life = False
extra_months = self.increase_in_asset_life % row.frequency_of_depreciation
if extra_months != 0:
self.calculate_last_schedule_date(self.asset_doc, row, extra_months)
# to help modify depreciation schedule when increase_in_asset_life is not a multiple of frequency_of_depreciation
def calculate_last_schedule_date(self, asset, row, extra_months):
asset.flags.increase_in_asset_life = True
number_of_pending_depreciations = cint(row.total_number_of_depreciations) - cint(
asset.opening_number_of_booked_depreciations
def get_depreciation_note(self):
return _("This schedule was created when Asset {0} was repaired through Asset Repair {1}.").format(
get_link_to_form(self.asset_doc.doctype, self.asset_doc.name),
get_link_to_form(self.doctype, self.name),
)
depr_schedule = get_depr_schedule(asset.name, "Active", row.finance_book)
def add_asset_activity(self, subject=None):
if not subject:
subject = _("Asset updated due to Asset Repair {0} {1}.").format(
get_link_to_form(self.doctype, self.name),
"submission" if self.docstatus == 1 else "cancellation",
)
# the Schedule Date in the final row of the old Depreciation Schedule
last_schedule_date = depr_schedule[len(depr_schedule) - 1].schedule_date
# the Schedule Date in the final row of the new Depreciation Schedule
asset.to_date = add_months(last_schedule_date, extra_months)
# the latest possible date at which the depreciation can occur, without increasing the Total Number of Depreciations
# if depreciations happen yearly and the Depreciation Posting Date is 01-01-2020, this could be 01-01-2021, 01-01-2022...
schedule_date = add_months(
row.depreciation_start_date,
number_of_pending_depreciations * cint(row.frequency_of_depreciation),
)
if asset.to_date > schedule_date:
row.total_number_of_depreciations += 1
def revert_depreciation_schedule_on_cancellation(self):
for row in self.asset_doc.finance_books:
row.total_number_of_depreciations -= self.increase_in_asset_life / row.frequency_of_depreciation
self.asset_doc.flags.increase_in_asset_life = False
extra_months = self.increase_in_asset_life % row.frequency_of_depreciation
if extra_months != 0:
self.calculate_last_schedule_date_before_modification(self.asset_doc, row, extra_months)
def calculate_last_schedule_date_before_modification(self, asset, row, extra_months):
asset.flags.increase_in_asset_life = True
number_of_pending_depreciations = cint(row.total_number_of_depreciations) - cint(
asset.opening_number_of_booked_depreciations
)
depr_schedule = get_depr_schedule(asset.name, "Active", row.finance_book)
# the Schedule Date in the final row of the modified Depreciation Schedule
last_schedule_date = depr_schedule[len(depr_schedule) - 1].schedule_date
# the Schedule Date in the final row of the original Depreciation Schedule
asset.to_date = add_months(last_schedule_date, -extra_months)
# the latest possible date at which the depreciation can occur, without decreasing the Total Number of Depreciations
# if depreciations happen yearly and the Depreciation Posting Date is 01-01-2020, this could be 01-01-2021, 01-01-2022...
schedule_date = add_months(
row.depreciation_start_date,
(number_of_pending_depreciations - 1) * cint(row.frequency_of_depreciation),
)
if asset.to_date < schedule_date:
row.total_number_of_depreciations -= 1
add_asset_activity(self.asset, subject)
@frappe.whitelist()
@@ -476,16 +384,11 @@ def get_downtime(failure_date, completion_date):
@frappe.whitelist()
def get_purchase_invoice(doctype, txt, searchfield, start, page_len, filters):
query = expense_item_pi_query(filters.get("company"))
return query.run(as_list=1)
def expense_item_pi_query(company):
PurchaseInvoice = DocType("Purchase Invoice")
PurchaseInvoiceItem = DocType("Purchase Invoice Item")
Item = DocType("Item")
query = (
return (
frappe.qb.from_(PurchaseInvoice)
.join(PurchaseInvoiceItem)
.on(PurchaseInvoiceItem.parent == PurchaseInvoice.name)
@@ -495,8 +398,18 @@ def expense_item_pi_query(company):
.where(
(Item.is_stock_item == 0)
& (Item.is_fixed_asset == 0)
& (PurchaseInvoice.company == company)
& (PurchaseInvoice.company == filters.get("company"))
& (PurchaseInvoice.docstatus == 1)
)
)
return query
).run(as_list=1)
@frappe.whitelist()
def get_expense_accounts(doctype, txt, searchfield, start, page_len, filters):
PurchaseInvoiceItem = DocType("Purchase Invoice Item")
return (
frappe.qb.from_(PurchaseInvoiceItem)
.select(PurchaseInvoiceItem.expense_account)
.distinct()
.where(PurchaseInvoiceItem.parent == filters.get("purchase_invoice"))
).run(as_list=1)

View File

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

View File

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

View File

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

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 (
get_asset_depr_schedule_doc,
get_asset_shift_factors_map,
get_temp_asset_depr_schedule_doc,
get_temp_depr_schedule_doc,
)
@@ -30,9 +30,7 @@ class AssetShiftAllocation(Document):
if TYPE_CHECKING:
from frappe.types import DF
from erpnext.assets.doctype.depreciation_schedule.depreciation_schedule import (
DepreciationSchedule,
)
from erpnext.assets.doctype.depreciation_schedule.depreciation_schedule import DepreciationSchedule
amended_from: DF.Link | None
asset: DF.Link
@@ -41,32 +39,138 @@ class AssetShiftAllocation(Document):
naming_series: DF.Literal["ACC-ASA-.YYYY.-"]
# end: auto-generated types
def after_insert(self):
self.fetch_and_set_depr_schedule()
def validate(self):
self.asset_depr_schedule_doc = get_asset_depr_schedule_doc(self.asset, "Active", self.finance_book)
if self.get("depreciation_schedule") and self.docstatus == 0:
self.validate_invalid_shift_change()
self.update_depr_schedule()
self.validate_invalid_shift_change()
self.update_depr_schedule()
def after_insert(self):
self.fetch_and_set_depr_schedule()
def on_submit(self):
self.create_new_asset_depr_schedule()
def validate_invalid_shift_change(self):
for i, sch in enumerate(self.depreciation_schedule):
if sch.journal_entry and self.asset_depr_schedule_doc.depreciation_schedule[i].shift != sch.shift:
frappe.throw(
_(
"Row {0}: Shift cannot be changed since the depreciation has already been processed"
).format(i)
)
def update_depr_schedule(self):
self.adjust_depr_shifts()
asset_doc = frappe.get_doc("Asset", self.asset)
fb_row = self.get_finance_book_row(asset_doc)
temp_depr_schedule_doc = get_temp_depr_schedule_doc(
asset_doc, fb_row, updated_depr_schedule=self.depreciation_schedule
)
# Update the depreciation schedule with the new shifts
self.depreciation_schedule = []
self.modify_depr_schedule(temp_depr_schedule_doc.get("depreciation_schedule"))
def adjust_depr_shifts(self):
"""
Adjust the shifts in the depreciation schedule based on the new shifts
"""
shift_factors_map = get_asset_shift_factors_map()
reverse_shift_factors_map = {v: k for k, v in shift_factors_map.items()}
factor_diff = self.calculate_shift_factor_diff(shift_factors_map)
# Case 1: Reduce shifts if there is an excess factor
if factor_diff > 0:
self.reduce_depr_shifts(factor_diff, shift_factors_map, reverse_shift_factors_map)
# Case 2: Add shifts if there is a missing factor
elif factor_diff < 0:
self.add_depr_shifts(factor_diff, shift_factors_map, reverse_shift_factors_map)
def calculate_shift_factor_diff(self, shift_factors_map):
original_shift_sum = sum(
shift_factors_map.get(schedule.shift, 0)
for schedule in self.asset_depr_schedule_doc.depreciation_schedule
)
new_shift_sum = sum(
shift_factors_map.get(schedule.shift, 0) for schedule in self.depreciation_schedule
)
return new_shift_sum - original_shift_sum
def reduce_depr_shifts(self, factor_diff, shift_factors_map, reverse_shift_factors_map):
for i, schedule in reversed(list(enumerate(self.depreciation_schedule))):
if factor_diff <= 0:
break
current_factor = shift_factors_map.get(schedule.shift, 0)
if current_factor <= factor_diff:
self.depreciation_schedule.pop(i)
factor_diff -= current_factor
else:
new_factor = current_factor - factor_diff
self.depreciation_schedule[i].shift = reverse_shift_factors_map.get(new_factor)
factor_diff = 0
def add_depr_shifts(self, factor_diff, shift_factors_map, reverse_shift_factors_map):
factor_diff = abs(factor_diff)
shift_factors = sorted(shift_factors_map.values(), reverse=True)
while factor_diff > 0:
for factor in shift_factors:
if factor <= factor_diff:
self.add_schedule_row(factor, reverse_shift_factors_map)
factor_diff -= factor
break
else:
frappe.throw(
_("Could not find a suitable shift to match the difference: {0}").format(factor_diff)
)
def add_schedule_row(self, factor, reverse_shift_factors_map):
schedule_date = add_months(
self.depreciation_schedule[-1].schedule_date,
cint(self.asset_depr_schedule_doc.frequency_of_depreciation),
)
if is_last_day_of_the_month(self.depreciation_schedule[-1].schedule_date):
schedule_date = get_last_day(schedule_date)
self.append(
"depreciation_schedule",
{
"schedule_date": schedule_date,
"shift": reverse_shift_factors_map.get(factor),
},
)
def get_finance_book_row(self, asset_doc):
idx = 0
for d in asset_doc.get("finance_books"):
if d.finance_book == self.finance_book:
idx = d.idx
break
return asset_doc.get("finance_books")[idx - 1]
def modify_depr_schedule(self, temp_depr_schedule):
for schedule in temp_depr_schedule:
self.append(
"depreciation_schedule",
{
"schedule_date": schedule.schedule_date,
"depreciation_amount": schedule.depreciation_amount,
"accumulated_depreciation_amount": schedule.accumulated_depreciation_amount,
"journal_entry": schedule.journal_entry,
"shift": schedule.shift,
},
)
def fetch_and_set_depr_schedule(self):
if self.asset_depr_schedule_doc:
if self.asset_depr_schedule_doc.shift_based:
for schedule in self.asset_depr_schedule_doc.get("depreciation_schedule"):
self.append(
"depreciation_schedule",
{
"schedule_date": schedule.schedule_date,
"depreciation_amount": schedule.depreciation_amount,
"accumulated_depreciation_amount": schedule.accumulated_depreciation_amount,
"journal_entry": schedule.journal_entry,
"shift": schedule.shift,
},
)
self.modify_depr_schedule(self.asset_depr_schedule_doc.depreciation_schedule)
self.flags.ignore_validate = True
self.save()
@@ -83,143 +187,6 @@ class AssetShiftAllocation(Document):
)
)
def validate_invalid_shift_change(self):
if not self.get("depreciation_schedule") or self.docstatus == 1:
return
for i, sch in enumerate(self.depreciation_schedule):
if sch.journal_entry and self.asset_depr_schedule_doc.depreciation_schedule[i].shift != sch.shift:
frappe.throw(
_(
"Row {0}: Shift cannot be changed since the depreciation has already been processed"
).format(i)
)
def update_depr_schedule(self):
if not self.get("depreciation_schedule") or self.docstatus == 1:
return
self.allocate_shift_diff_in_depr_schedule()
asset_doc = frappe.get_doc("Asset", self.asset)
fb_row = asset_doc.finance_books[self.asset_depr_schedule_doc.finance_book_id - 1]
asset_doc.flags.shift_allocation = True
temp_depr_schedule = get_temp_asset_depr_schedule_doc(
asset_doc, fb_row, new_depr_schedule=self.depreciation_schedule
).get("depreciation_schedule")
self.depreciation_schedule = []
for schedule in temp_depr_schedule:
self.append(
"depreciation_schedule",
{
"schedule_date": schedule.schedule_date,
"depreciation_amount": schedule.depreciation_amount,
"accumulated_depreciation_amount": schedule.accumulated_depreciation_amount,
"journal_entry": schedule.journal_entry,
"shift": schedule.shift,
},
)
def allocate_shift_diff_in_depr_schedule(self):
asset_shift_factors_map = get_asset_shift_factors_map()
reverse_asset_shift_factors_map = {asset_shift_factors_map[k]: k for k in asset_shift_factors_map}
original_shift_factors_sum = sum(
flt(asset_shift_factors_map.get(schedule.shift))
for schedule in self.asset_depr_schedule_doc.depreciation_schedule
)
new_shift_factors_sum = sum(
flt(asset_shift_factors_map.get(schedule.shift)) for schedule in self.depreciation_schedule
)
diff = new_shift_factors_sum - original_shift_factors_sum
if diff > 0:
for i, schedule in reversed(list(enumerate(self.depreciation_schedule))):
if diff <= 0:
break
shift_factor = flt(asset_shift_factors_map.get(schedule.shift))
if shift_factor <= diff:
self.depreciation_schedule.pop()
diff -= shift_factor
else:
try:
self.depreciation_schedule[i].shift = reverse_asset_shift_factors_map.get(
shift_factor - diff
)
diff = 0
except Exception:
frappe.throw(
_("Could not auto update shifts. Shift with shift factor {0} needed.")
).format(shift_factor - diff)
elif diff < 0:
shift_factors = list(asset_shift_factors_map.values())
desc_shift_factors = sorted(shift_factors, reverse=True)
depr_schedule_len_diff = self.asset_depr_schedule_doc.total_number_of_depreciations - len(
self.depreciation_schedule
)
subsets_result = []
if depr_schedule_len_diff > 0:
num_rows_to_add = depr_schedule_len_diff
while not subsets_result and num_rows_to_add > 0:
find_subsets_with_sum(shift_factors, num_rows_to_add, abs(diff), [], subsets_result)
if subsets_result:
break
num_rows_to_add -= 1
if subsets_result:
for i in range(num_rows_to_add):
schedule_date = add_months(
self.depreciation_schedule[-1].schedule_date,
cint(self.asset_depr_schedule_doc.frequency_of_depreciation),
)
if is_last_day_of_the_month(self.depreciation_schedule[-1].schedule_date):
schedule_date = get_last_day(schedule_date)
self.append(
"depreciation_schedule",
{
"schedule_date": schedule_date,
"shift": reverse_asset_shift_factors_map.get(subsets_result[0][i]),
},
)
if depr_schedule_len_diff <= 0 or not subsets_result:
for i, schedule in reversed(list(enumerate(self.depreciation_schedule))):
diff = abs(diff)
if diff <= 0:
break
shift_factor = flt(asset_shift_factors_map.get(schedule.shift))
if shift_factor <= diff:
for sf in desc_shift_factors:
if sf - shift_factor <= diff:
self.depreciation_schedule[i].shift = reverse_asset_shift_factors_map.get(sf)
diff -= sf - shift_factor
break
else:
try:
self.depreciation_schedule[i].shift = reverse_asset_shift_factors_map.get(
shift_factor + diff
)
diff = 0
except Exception:
frappe.throw(
_("Could not auto update shifts. Shift with shift factor {0} needed.")
).format(shift_factor + diff)
def create_new_asset_depr_schedule(self):
new_asset_depr_schedule_doc = frappe.copy_doc(self.asset_depr_schedule_doc)
@@ -257,17 +224,3 @@ class AssetShiftAllocation(Document):
get_link_to_form(self.doctype, self.name)
),
)
def find_subsets_with_sum(numbers, k, target_sum, current_subset, result):
if k == 0 and target_sum == 0:
result.append(current_subset.copy())
return
if k <= 0 or target_sum <= 0 or not numbers:
return
# Include the current number in the subset
find_subsets_with_sum(numbers, k - 1, target_sum - numbers[0], [*current_subset, numbers[0]], result)
# Exclude the current number from the subset
find_subsets_with_sum(numbers[1:], k, target_sum, current_subset, result)

View File

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

View File

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

View File

@@ -5,7 +5,7 @@
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import flt, formatdate, get_link_to_form, getdate
from frappe.utils import cstr, flt, formatdate, get_link_to_form, getdate
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_checks_for_pl_and_bs_accounts,
@@ -14,7 +14,7 @@ from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciatio
from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
make_new_active_asset_depr_schedules_and_cancel_current_ones,
reschedule_depreciation,
)
@@ -46,10 +46,26 @@ class AssetValueAdjustment(Document):
self.set_current_asset_value()
self.set_difference_amount()
def validate_date(self):
asset_purchase_date = frappe.db.get_value("Asset", self.asset, "purchase_date")
if getdate(self.date) < getdate(asset_purchase_date):
frappe.throw(
_("Asset Value Adjustment cannot be posted before Asset's purchase date <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):
self.make_depreciation_entry()
self.set_value_after_depreciation()
self.update_asset(self.new_asset_value)
self.make_asset_revaluation_entry()
self.update_asset()
add_asset_activity(
self.asset,
_("Asset's value adjusted after submission of Asset Value Adjustment {0}").format(
@@ -67,27 +83,7 @@ class AssetValueAdjustment(Document):
),
)
def validate_date(self):
asset_purchase_date = frappe.db.get_value("Asset", self.asset, "purchase_date")
if getdate(self.date) < getdate(asset_purchase_date):
frappe.throw(
_("Asset Value Adjustment cannot be posted before Asset's purchase date <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):
def make_asset_revaluation_entry(self):
asset = frappe.get_doc("Asset", self.asset)
(
fixed_asset_account,
@@ -114,46 +110,15 @@ class AssetValueAdjustment(Document):
}
if self.difference_amount < 0:
credit_entry = {
"account": fixed_asset_account,
"credit_in_account_currency": -self.difference_amount,
**entry_template,
}
debit_entry = {
"account": self.difference_account,
"debit_in_account_currency": -self.difference_amount,
**entry_template,
}
credit_entry, debit_entry = self.get_entry_for_asset_value_decrease(
fixed_asset_account, entry_template
)
elif self.difference_amount > 0:
credit_entry = {
"account": self.difference_account,
"credit_in_account_currency": self.difference_amount,
**entry_template,
}
debit_entry = {
"account": fixed_asset_account,
"debit_in_account_currency": self.difference_amount,
**entry_template,
}
credit_entry, debit_entry = self.get_entry_for_asset_value_increase(
fixed_asset_account, entry_template
)
accounting_dimensions = get_checks_for_pl_and_bs_accounts()
for dimension in accounting_dimensions:
if dimension.get("mandatory_for_bs"):
credit_entry.update(
{
dimension["fieldname"]: self.get(dimension["fieldname"])
or dimension.get("default_dimension")
}
)
if dimension.get("mandatory_for_pl"):
debit_entry.update(
{
dimension["fieldname"]: self.get(dimension["fieldname"])
or dimension.get("default_dimension")
}
)
self.update_accounting_dimensions(credit_entry, debit_entry)
je.append("accounts", credit_entry)
je.append("accounts", debit_entry)
@@ -163,40 +128,81 @@ class AssetValueAdjustment(Document):
self.db_set("journal_entry", je.name)
def update_asset(self, asset_value=None):
def get_entry_for_asset_value_decrease(self, fixed_asset_account, entry_template):
credit_entry = {
"account": fixed_asset_account,
"credit_in_account_currency": -self.difference_amount,
**entry_template,
}
debit_entry = {
"account": self.difference_account,
"debit_in_account_currency": -self.difference_amount,
**entry_template,
}
return credit_entry, debit_entry
def get_entry_for_asset_value_increase(self, fixed_asset_account, entry_template):
credit_entry = {
"account": self.difference_account,
"credit_in_account_currency": self.difference_amount,
**entry_template,
}
debit_entry = {
"account": fixed_asset_account,
"debit_in_account_currency": self.difference_amount,
**entry_template,
}
return credit_entry, debit_entry
def update_accounting_dimensions(self, credit_entry, debit_entry):
accounting_dimensions = get_checks_for_pl_and_bs_accounts()
for dimension in accounting_dimensions:
dimension_value = self.get(dimension["fieldname"]) or dimension.get("default_dimension")
if dimension.get("mandatory_for_bs"):
credit_entry.update({dimension["fieldname"]: dimension_value})
if dimension.get("mandatory_for_pl"):
debit_entry.update({dimension["fieldname"]: dimension_value})
def update_asset(self):
asset = self.update_asset_value_after_depreciation()
note = self.get_adjustment_note()
reschedule_depreciation(asset, note)
def update_asset_value_after_depreciation(self):
difference_amount = self.difference_amount if self.docstatus == 1 else -1 * self.difference_amount
asset = frappe.get_doc("Asset", self.asset)
if asset.calculate_depreciation:
for row in asset.finance_books:
if cstr(row.finance_book) == cstr(self.finance_book):
row.value_after_depreciation += flt(difference_amount)
row.db_update()
if not asset.calculate_depreciation:
asset.value_after_depreciation = asset_value
asset.save()
return
asset.flags.decrease_in_asset_value_due_to_value_adjustment = True
asset.value_after_depreciation += flt(difference_amount)
asset.db_update()
return asset
def get_adjustment_note(self):
if self.docstatus == 1:
notes = _(
"This schedule was created when Asset {0} was adjusted through Asset Value Adjustment {1}."
).format(
get_link_to_form("Asset", asset.name),
get_link_to_form("Asset", self.asset),
get_link_to_form(self.get("doctype"), self.get("name")),
)
elif self.docstatus == 2:
notes = _(
"This schedule was created when Asset {0}'s Asset Value Adjustment {1} was cancelled."
).format(
get_link_to_form("Asset", asset.name),
get_link_to_form("Asset", self.asset),
get_link_to_form(self.get("doctype"), self.get("name")),
)
make_new_active_asset_depr_schedules_and_cancel_current_ones(
asset,
notes,
value_after_depreciation=asset_value,
ignore_booked_entry=True,
difference_amount=self.difference_amount,
)
asset.flags.ignore_validate_update_after_submit = True
asset.save()
return notes
@frappe.whitelist()

View File

@@ -114,12 +114,12 @@ class TestAssetValueAdjustment(IntegrationTestCase):
["2023-05-31", 9983.33, 45408.05],
["2023-06-30", 9983.33, 55391.38],
["2023-07-31", 9983.33, 65374.71],
["2023-08-31", 8300.0, 73674.71],
["2023-09-30", 8300.0, 81974.71],
["2023-10-31", 8300.0, 90274.71],
["2023-11-30", 8300.0, 98574.71],
["2023-12-31", 8300.0, 106874.71],
["2024-01-15", 8300.0, 115174.71],
["2023-08-31", 9070.36, 74445.07],
["2023-09-30", 9070.36, 83515.43],
["2023-10-31", 9070.36, 92585.79],
["2023-11-30", 9070.36, 101656.15],
["2023-12-31", 9070.36, 110726.51],
["2024-01-15", 4448.2, 115174.71],
]
schedules = [
@@ -154,7 +154,11 @@ class TestAssetValueAdjustment(IntegrationTestCase):
# create asset repair
asset_repair = create_asset_repair(
asset=asset_doc, capitalize_repair_cost=1, item="_Test Non Stock Item", submit=1
asset=asset_doc,
capitalize_repair_cost=1,
item="_Test Non Stock Item",
submit=1,
increase_in_asset_life=1,
)
first_asset_depr_schedule = get_asset_depr_schedule_doc(asset_doc.name, "Active")
@@ -201,24 +205,24 @@ class TestAssetValueAdjustment(IntegrationTestCase):
["2023-05-31", 9983.33, 45408.05],
["2023-06-30", 9983.33, 55391.38],
["2023-07-31", 9983.33, 65374.71],
["2023-08-31", 2766.67, 68141.38],
["2023-09-30", 2766.67, 70908.05],
["2023-10-31", 2766.67, 73674.72],
["2023-11-30", 2766.67, 76441.39],
["2023-12-31", 2766.67, 79208.06],
["2024-01-31", 2766.67, 81974.73],
["2024-02-29", 2766.67, 84741.4],
["2024-03-31", 2766.67, 87508.07],
["2024-04-30", 2766.67, 90274.74],
["2024-05-31", 2766.67, 93041.41],
["2024-06-30", 2766.67, 95808.08],
["2024-07-31", 2766.67, 98574.75],
["2024-08-31", 2766.67, 101341.42],
["2024-09-30", 2766.67, 104108.09],
["2024-10-31", 2766.67, 106874.76],
["2024-11-30", 2766.67, 109641.43],
["2024-12-31", 2766.67, 112408.1],
["2025-01-15", 2766.61, 115174.71],
["2023-08-31", 2847.27, 68221.98],
["2023-09-30", 2847.27, 71069.25],
["2023-10-31", 2847.27, 73916.52],
["2023-11-30", 2847.27, 76763.79],
["2023-12-31", 2847.27, 79611.06],
["2024-01-31", 2847.27, 82458.33],
["2024-02-29", 2847.27, 85305.6],
["2024-03-31", 2847.27, 88152.87],
["2024-04-30", 2847.27, 91000.14],
["2024-05-31", 2847.27, 93847.41],
["2024-06-30", 2847.27, 96694.68],
["2024-07-31", 2847.27, 99541.95],
["2024-08-31", 2847.27, 102389.22],
["2024-09-30", 2847.27, 105236.49],
["2024-10-31", 2847.27, 108083.76],
["2024-11-30", 2847.27, 110931.03],
["2024-12-31", 2847.27, 113778.3],
["2025-01-31", 1396.41, 115174.71],
]
schedules = [
@@ -246,12 +250,12 @@ class TestAssetValueAdjustment(IntegrationTestCase):
["2023-05-31", 9983.33, 45408.05],
["2023-06-30", 9983.33, 55391.38],
["2023-07-31", 9983.33, 65374.71],
["2023-08-31", 8208.33, 73583.04],
["2023-09-30", 8208.33, 81791.37],
["2023-10-31", 8208.33, 89999.7],
["2023-11-30", 8208.33, 98208.03],
["2023-12-31", 8208.33, 106416.36],
["2024-01-15", 8208.35, 114624.71],
["2023-08-31", 8970.18, 74344.89],
["2023-09-30", 8970.18, 83315.07],
["2023-10-31", 8970.18, 92285.25],
["2023-11-30", 8970.18, 101255.43],
["2023-12-31", 8970.18, 110225.61],
["2024-01-15", 4399.1, 114624.71],
]
schedules = [
@@ -262,7 +266,7 @@ class TestAssetValueAdjustment(IntegrationTestCase):
self.assertEqual(schedules, expected_schedules)
def test_difference_amount(self):
pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=120000.0, location="Test Location")
pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location")
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name")
asset_doc = frappe.get_doc("Asset", asset_name)
@@ -282,17 +286,18 @@ class TestAssetValueAdjustment(IntegrationTestCase):
)
asset_doc.submit()
current_asset_value = get_asset_value_after_depreciation(asset_doc.name)
adj_doc = make_asset_value_adjustment(
asset=asset_doc.name,
current_asset_value=54000,
new_asset_value=50000.0,
current_asset_value=current_asset_value,
new_asset_value=40000,
date="2023-08-21",
)
adj_doc.submit()
difference_amount = adj_doc.new_asset_value - adj_doc.current_asset_value
self.assertEqual(difference_amount, -4000)
self.assertEqual(difference_amount, -60000)
asset_doc.load_from_db()
self.assertEqual(asset_doc.value_after_depreciation, 50000.0)
self.assertEqual(asset_doc.finance_books[0].value_after_depreciation, 40000.0)
def make_asset_value_adjustment(**args):

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
execute:frappe.db.set_single_value("Accounts Settings", "receivable_payable_fetch_method", "Buffered Cursor")
erpnext.patches.v14_0.set_update_price_list_based_on
erpnext.patches.v15_0.update_journal_entry_type
erpnext.patches.v15_0.set_grand_total_to_default_mop

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