feat: depreciate asset after sale (#26543)
This commit is contained in:
parent
f037bae8ea
commit
54c31ed33c
@ -4,7 +4,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import frappe, erpnext
|
import frappe, erpnext
|
||||||
import frappe.defaults
|
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 frappe import _, msgprint, throw
|
||||||
from erpnext.accounts.party import get_party_account, get_due_date, get_party_details
|
from erpnext.accounts.party import get_party_account, get_due_date, get_party_details
|
||||||
from frappe.model.mapper import get_mapped_doc
|
from frappe.model.mapper import get_mapped_doc
|
||||||
@ -13,7 +13,7 @@ from erpnext.accounts.utils import get_account_currency
|
|||||||
from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amount_based_on_so
|
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.projects.doctype.timesheet.timesheet import get_projectwise_timesheet_data
|
||||||
from erpnext.assets.doctype.asset.depreciation \
|
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.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.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
|
from erpnext.setup.doctype.company.company import update_company_current_month_sales
|
||||||
@ -920,27 +920,24 @@ class SalesInvoice(SellingController):
|
|||||||
for item in self.get("items"):
|
for item in self.get("items"):
|
||||||
if flt(item.base_net_amount, item.precision("base_net_amount")):
|
if flt(item.base_net_amount, item.precision("base_net_amount")):
|
||||||
if item.is_fixed_asset:
|
if item.is_fixed_asset:
|
||||||
if item.get('asset'):
|
asset = self.get_asset(item)
|
||||||
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))
|
|
||||||
|
|
||||||
if self.is_return:
|
if self.is_return:
|
||||||
fixed_asset_gl_entries = get_gl_entries_on_asset_regain(asset,
|
fixed_asset_gl_entries = get_gl_entries_on_asset_regain(asset,
|
||||||
item.base_net_amount, item.finance_book)
|
item.base_net_amount, item.finance_book)
|
||||||
asset.db_set("disposal_date", None)
|
asset.db_set("disposal_date", None)
|
||||||
|
|
||||||
|
if asset.calculate_depreciation:
|
||||||
|
self.reset_depreciation_schedule(asset)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(asset,
|
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(asset,
|
||||||
item.base_net_amount, item.finance_book)
|
item.base_net_amount, item.finance_book)
|
||||||
asset.db_set("disposal_date", self.posting_date)
|
asset.db_set("disposal_date", self.posting_date)
|
||||||
|
|
||||||
|
if asset.calculate_depreciation:
|
||||||
|
self.depreciate_asset(asset)
|
||||||
|
|
||||||
for gle in fixed_asset_gl_entries:
|
for gle in fixed_asset_gl_entries:
|
||||||
gle["against"] = self.customer
|
gle["against"] = self.customer
|
||||||
gl_entries.append(self.get_gl_dict(gle, item=item))
|
gl_entries.append(self.get_gl_dict(gle, item=item))
|
||||||
@ -972,6 +969,89 @@ class SalesInvoice(SellingController):
|
|||||||
erpnext.is_perpetual_inventory_enabled(self.company):
|
erpnext.is_perpetual_inventory_enabled(self.company):
|
||||||
gl_entries += super(SalesInvoice, self).get_gl_entries()
|
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):
|
def set_asset_status(self, asset):
|
||||||
if self.is_return:
|
if self.is_return:
|
||||||
asset.set_status()
|
asset.set_status()
|
||||||
|
|||||||
@ -12,6 +12,7 @@ from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import unli
|
|||||||
from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import WarehouseMissingError
|
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.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.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.exceptions import InvalidAccountCurrency, InvalidCurrency
|
||||||
from erpnext.stock.doctype.serial_no.serial_no import SerialNoWarehouseError
|
from erpnext.stock.doctype.serial_no.serial_no import SerialNoWarehouseError
|
||||||
from frappe.model.naming import make_autoname
|
from frappe.model.naming import make_autoname
|
||||||
@ -2101,6 +2102,30 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
sales_invoice.save()
|
sales_invoice.save()
|
||||||
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC")
|
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():
|
def get_sales_invoice_for_e_invoice():
|
||||||
si = make_sales_invoice_for_ewaybill()
|
si = make_sales_invoice_for_ewaybill()
|
||||||
si.naming_series = 'INV-2020-.#####'
|
si.naming_series = 'INV-2020-.#####'
|
||||||
|
|||||||
@ -56,12 +56,12 @@ class Asset(AccountsController):
|
|||||||
if self.is_existing_asset and self.purchase_invoice:
|
if self.is_existing_asset and self.purchase_invoice:
|
||||||
frappe.throw(_("Purchase Invoice cannot be made against an existing asset {0}").format(self.name))
|
frappe.throw(_("Purchase Invoice cannot be made against an existing asset {0}").format(self.name))
|
||||||
|
|
||||||
def prepare_depreciation_data(self):
|
def prepare_depreciation_data(self, date_of_sale=None):
|
||||||
if self.calculate_depreciation:
|
if self.calculate_depreciation:
|
||||||
self.value_after_depreciation = 0
|
self.value_after_depreciation = 0
|
||||||
self.set_depreciation_rate()
|
self.set_depreciation_rate()
|
||||||
self.make_depreciation_schedule()
|
self.make_depreciation_schedule(date_of_sale)
|
||||||
self.set_accumulated_depreciation()
|
self.set_accumulated_depreciation(date_of_sale)
|
||||||
else:
|
else:
|
||||||
self.finance_books = []
|
self.finance_books = []
|
||||||
self.value_after_depreciation = (flt(self.gross_purchase_amount) -
|
self.value_after_depreciation = (flt(self.gross_purchase_amount) -
|
||||||
@ -167,7 +167,7 @@ class Asset(AccountsController):
|
|||||||
d.rate_of_depreciation = flt(self.get_depreciation_rate(d, on_validate=True),
|
d.rate_of_depreciation = flt(self.get_depreciation_rate(d, on_validate=True),
|
||||||
d.precision("rate_of_depreciation"))
|
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:
|
if 'Manual' not in [d.depreciation_method for d in self.finance_books] and not self.schedules:
|
||||||
self.schedules = []
|
self.schedules = []
|
||||||
|
|
||||||
@ -212,6 +212,21 @@ class Asset(AccountsController):
|
|||||||
# so monthly schedule date is calculated by removing 11 months from it
|
# so monthly schedule date is calculated by removing 11 months from it
|
||||||
monthly_schedule_date = add_months(schedule_date, - d.frequency_of_depreciation + 1)
|
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
|
# For first row
|
||||||
if has_pro_rata and n==0:
|
if has_pro_rata and n==0:
|
||||||
depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount,
|
depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount,
|
||||||
@ -303,6 +318,21 @@ class Asset(AccountsController):
|
|||||||
break
|
break
|
||||||
return start
|
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
|
# if it returns True, depreciation_amount will not be equal for the first and last rows
|
||||||
def check_is_pro_rata(self, row):
|
def check_is_pro_rata(self, row):
|
||||||
@ -357,7 +387,7 @@ class Asset(AccountsController):
|
|||||||
frappe.throw(_("Depreciation Row {0}: Next Depreciation Date cannot be before Available-for-use Date")
|
frappe.throw(_("Depreciation Row {0}: Next Depreciation Date cannot be before Available-for-use Date")
|
||||||
.format(row.idx))
|
.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']
|
straight_line_idx = [d.idx for d in self.get("schedules") if d.depreciation_method == 'Straight Line']
|
||||||
finance_books = []
|
finance_books = []
|
||||||
|
|
||||||
@ -365,7 +395,7 @@ class Asset(AccountsController):
|
|||||||
if ignore_booked_entry and d.journal_entry:
|
if ignore_booked_entry and d.journal_entry:
|
||||||
continue
|
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)
|
accumulated_depreciation = flt(self.opening_accumulated_depreciation)
|
||||||
value_after_depreciation = flt(self.get_value_after_depreciation(d.finance_book_id))
|
value_after_depreciation = flt(self.get_value_after_depreciation(d.finance_book_id))
|
||||||
finance_books.append(int(d.finance_book_id))
|
finance_books.append(int(d.finance_book_id))
|
||||||
@ -374,7 +404,7 @@ class Asset(AccountsController):
|
|||||||
value_after_depreciation -= flt(depreciation_amount)
|
value_after_depreciation -= flt(depreciation_amount)
|
||||||
|
|
||||||
# for the last row, if depreciation method = Straight Line
|
# 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]
|
book = self.get('finance_books')[cint(d.finance_book_id) - 1]
|
||||||
depreciation_amount += flt(value_after_depreciation -
|
depreciation_amount += flt(value_after_depreciation -
|
||||||
flt(book.expected_value_after_useful_life), d.precision("depreciation_amount"))
|
flt(book.expected_value_after_useful_life), d.precision("depreciation_amount"))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user