feat: depreciate asset after sale (#26543)
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index cecc1a1..51548e9 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -4,7 +4,7 @@
from __future__ import unicode_literals
import frappe, erpnext
import frappe.defaults
-from frappe.utils import cint, flt, getdate, add_days, cstr, nowdate, get_link_to_form, formatdate
+from frappe.utils import cint, flt, getdate, add_days, add_months, cstr, nowdate, get_link_to_form, formatdate
from frappe import _, msgprint, throw
from erpnext.accounts.party import get_party_account, get_due_date, get_party_details
from frappe.model.mapper import get_mapped_doc
@@ -13,7 +13,7 @@
from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amount_based_on_so
from erpnext.projects.doctype.timesheet.timesheet import get_projectwise_timesheet_data
from erpnext.assets.doctype.asset.depreciation \
- import get_disposal_account_and_cost_center, get_gl_entries_on_asset_disposal, get_gl_entries_on_asset_regain
+ import get_disposal_account_and_cost_center, get_gl_entries_on_asset_disposal, get_gl_entries_on_asset_regain, post_depreciation_entries
from erpnext.stock.doctype.batch.batch import set_batch_nos
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos, get_delivery_note_serial_no
from erpnext.setup.doctype.company.company import update_company_current_month_sales
@@ -920,27 +920,24 @@
for item in self.get("items"):
if flt(item.base_net_amount, item.precision("base_net_amount")):
if item.is_fixed_asset:
- if item.get('asset'):
- asset = frappe.get_doc("Asset", item.asset)
- else:
- frappe.throw(_(
- "Row #{0}: You must select an Asset for Item {1}.").format(item.idx, item.item_name),
- title=_("Missing Asset")
- )
- if (len(asset.finance_books) > 1 and not item.finance_book
- and asset.finance_books[0].finance_book):
- frappe.throw(_("Select finance book for the item {0} at row {1}")
- .format(item.item_code, item.idx))
+ 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)
asset.db_set("disposal_date", None)
+
+ if asset.calculate_depreciation:
+ self.reset_depreciation_schedule(asset)
+
else:
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(asset,
item.base_net_amount, item.finance_book)
asset.db_set("disposal_date", self.posting_date)
+ if asset.calculate_depreciation:
+ self.depreciate_asset(asset)
+
for gle in fixed_asset_gl_entries:
gle["against"] = self.customer
gl_entries.append(self.get_gl_dict(gle, item=item))
@@ -972,6 +969,89 @@
erpnext.is_perpetual_inventory_enabled(self.company):
gl_entries += super(SalesInvoice, self).get_gl_entries()
+ def get_asset(self, item):
+ if item.get('asset'):
+ asset = frappe.get_doc("Asset", item.asset)
+ else:
+ frappe.throw(_(
+ "Row #{0}: You must select an Asset for Item {1}.").format(item.idx, item.item_name),
+ title=_("Missing Asset")
+ )
+
+ self.check_finance_books(item, asset)
+ return asset
+
+ def check_finance_books(self, item, asset):
+ if (len(asset.finance_books) > 1 and not item.finance_book
+ and asset.finance_books[0].finance_book):
+ frappe.throw(_("Select finance book for the item {0} at row {1}")
+ .format(item.item_code, item.idx))
+
+ def depreciate_asset(self, asset):
+ asset.flags.ignore_validate_update_after_submit = True
+ asset.prepare_depreciation_data(self.posting_date)
+ asset.save()
+
+ post_depreciation_entries(self.posting_date)
+
+ def reset_depreciation_schedule(self, asset):
+ asset.flags.ignore_validate_update_after_submit = True
+
+ # recreate original depreciation schedule of the asset
+ asset.prepare_depreciation_data()
+
+ self.modify_depreciation_schedule_for_asset_repairs(asset)
+ asset.save()
+
+ self.delete_depreciation_entry_made_after_sale(asset)
+
+ def modify_depreciation_schedule_for_asset_repairs(self, asset):
+ asset_repairs = frappe.get_all(
+ 'Asset Repair',
+ filters = {'asset': asset.name},
+ fields = ['name', 'increase_in_asset_life']
+ )
+
+ for repair in asset_repairs:
+ if repair.increase_in_asset_life:
+ asset_repair = frappe.get_doc('Asset Repair', repair.name)
+ asset_repair.modify_depreciation_schedule()
+ asset.prepare_depreciation_data()
+
+ def delete_depreciation_entry_made_after_sale(self, asset):
+ from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
+
+ posting_date_of_original_invoice = self.get_posting_date_of_sales_invoice()
+
+ row = -1
+ finance_book = asset.get('schedules')[0].get('finance_book')
+ for schedule in asset.get('schedules'):
+ if schedule.finance_book != finance_book:
+ row = 0
+ finance_book = schedule.finance_book
+ else:
+ row += 1
+
+ if schedule.schedule_date == posting_date_of_original_invoice:
+ if not self.sale_was_made_on_original_schedule_date(asset, schedule, row, posting_date_of_original_invoice):
+ reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry)
+ reverse_journal_entry.posting_date = nowdate()
+ reverse_journal_entry.submit()
+
+ def get_posting_date_of_sales_invoice(self):
+ return frappe.db.get_value('Sales Invoice', self.return_against, 'posting_date')
+
+ # if the invoice had been posted on the date the depreciation was initially supposed to happen, the depreciation shouldn't be undone
+ def sale_was_made_on_original_schedule_date(self, asset, schedule, row, posting_date_of_original_invoice):
+ for finance_book in asset.get('finance_books'):
+ if schedule.finance_book == finance_book.finance_book:
+ orginal_schedule_date = add_months(finance_book.depreciation_start_date,
+ row * cint(finance_book.frequency_of_depreciation))
+
+ if orginal_schedule_date == posting_date_of_original_invoice:
+ return True
+ return False
+
def set_asset_status(self, asset):
if self.is_return:
asset.set_status()
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index be20b18..f98275b 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -12,6 +12,7 @@
from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import WarehouseMissingError
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data
+from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries
from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency
from erpnext.stock.doctype.serial_no.serial_no import SerialNoWarehouseError
from frappe.model.naming import make_autoname
@@ -2101,6 +2102,30 @@
sales_invoice.save()
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC")
+ def test_asset_depreciation_on_sale(self):
+ """
+ Tests if an Asset set to depreciate yearly on June 30, that gets sold on Sept 30, creates an additional depreciation entry on Sept 30.
+ """
+
+ create_asset_data()
+ asset = create_asset(item_code="Macbook Pro", calculate_depreciation=1, submit=1)
+ post_depreciation_entries(getdate("2021-09-30"))
+
+ create_sales_invoice(item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000, posting_date=getdate("2021-09-30"))
+ asset.load_from_db()
+
+ expected_values = [
+ ["2020-06-30", 1311.48, 1311.48],
+ ["2021-06-30", 20000.0, 21311.48],
+ ["2021-09-30", 3966.76, 25278.24]
+ ]
+
+ for i, schedule in enumerate(asset.schedules):
+ 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.assertTrue(schedule.journal_entry)
+
def get_sales_invoice_for_e_invoice():
si = make_sales_invoice_for_ewaybill()
si.naming_series = 'INV-2020-.#####'
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index 66f0bdc..b7ca693 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -56,12 +56,12 @@
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):
+ def prepare_depreciation_data(self, date_of_sale=None):
if self.calculate_depreciation:
self.value_after_depreciation = 0
self.set_depreciation_rate()
- self.make_depreciation_schedule()
- self.set_accumulated_depreciation()
+ self.make_depreciation_schedule(date_of_sale)
+ self.set_accumulated_depreciation(date_of_sale)
else:
self.finance_books = []
self.value_after_depreciation = (flt(self.gross_purchase_amount) -
@@ -167,7 +167,7 @@
d.rate_of_depreciation = flt(self.get_depreciation_rate(d, on_validate=True),
d.precision("rate_of_depreciation"))
- def make_depreciation_schedule(self):
+ def make_depreciation_schedule(self, date_of_sale):
if 'Manual' not in [d.depreciation_method for d in self.finance_books] and not self.schedules:
self.schedules = []
@@ -212,6 +212,21 @@
# so monthly schedule date is calculated by removing 11 months from it
monthly_schedule_date = add_months(schedule_date, - d.frequency_of_depreciation + 1)
+ # if asset is being sold
+ if date_of_sale:
+ from_date = self.get_from_date(d.finance_book)
+ depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount,
+ from_date, date_of_sale)
+
+ self.append("schedules", {
+ "schedule_date": date_of_sale,
+ "depreciation_amount": depreciation_amount,
+ "depreciation_method": d.depreciation_method,
+ "finance_book": d.finance_book,
+ "finance_book_id": d.idx
+ })
+ break
+
# For first row
if has_pro_rata and n==0:
depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount,
@@ -303,6 +318,21 @@
break
return start
+ def get_from_date(self, finance_book):
+ if not self.get('schedules'):
+ return self.available_for_use_date
+
+ if len(self.finance_books) == 1:
+ return self.schedules[-1].schedule_date
+
+ from_date = ""
+ for schedule in self.get('schedules'):
+ if schedule.finance_book == finance_book:
+ from_date = schedule.schedule_date
+
+ if from_date:
+ return from_date
+ return self.available_for_use_date
# if it returns True, depreciation_amount will not be equal for the first and last rows
def check_is_pro_rata(self, row):
@@ -357,7 +387,7 @@
frappe.throw(_("Depreciation Row {0}: Next Depreciation Date cannot be before Available-for-use Date")
.format(row.idx))
- def set_accumulated_depreciation(self, ignore_booked_entry = False):
+ def set_accumulated_depreciation(self, date_of_sale=None, ignore_booked_entry = False):
straight_line_idx = [d.idx for d in self.get("schedules") if d.depreciation_method == 'Straight Line']
finance_books = []
@@ -365,7 +395,7 @@
if ignore_booked_entry and d.journal_entry:
continue
- if d.finance_book_id not in finance_books:
+ if int(d.finance_book_id) not in finance_books:
accumulated_depreciation = flt(self.opening_accumulated_depreciation)
value_after_depreciation = flt(self.get_value_after_depreciation(d.finance_book_id))
finance_books.append(int(d.finance_book_id))
@@ -374,7 +404,7 @@
value_after_depreciation -= flt(depreciation_amount)
# for the last row, if depreciation method = Straight Line
- if straight_line_idx and i == max(straight_line_idx) - 1:
+ if straight_line_idx and i == max(straight_line_idx) - 1 and not date_of_sale:
book = self.get('finance_books')[cint(d.finance_book_id) - 1]
depreciation_amount += flt(value_after_depreciation -
flt(book.expected_value_after_useful_life), d.precision("depreciation_amount"))