Fixed asset: status, restore, testcase and much more

This commit is contained in:
Nabin Hait 2016-03-11 15:42:23 +05:30
parent 4d668dbaa3
commit 7536349f17
13 changed files with 307 additions and 71 deletions

View File

@ -17,10 +17,16 @@ frappe.ui.form.on('Asset', {
},
refresh: function(frm) {
if(frm.doc.docstatus==1 && frm.doc.status=='Available') {
cur_frm.add_custom_button("Scrap", function() {
erpnext.asset.scrap_asset(frm);
});
if (frm.doc.docstatus==1) {
if (in_list(["Submittted", "Partially Depreciated", "Fully Depreciated"], frm.doc.status)) {
cur_frm.add_custom_button("Scrap Asset", function() {
erpnext.asset.scrap_asset(frm);
});
} else if (frm.doc.status=='Scrapped') {
cur_frm.add_custom_button("Restore Asset", function() {
erpnext.asset.restore_asset(frm);
});
}
}
}
});
@ -37,4 +43,18 @@ erpnext.asset.scrap_asset = function(frm) {
}
})
})
}
erpnext.asset.restore_asset = function(frm) {
frappe.confirm(__("Do you really want to restore this scrapped asset?"), function () {
frappe.call({
args: {
"asset_name": frm.doc.name
},
method: "erpnext.accounts.doctype.asset.depreciation.restore_asset",
callback: function(r) {
cur_frm.reload_doc();
}
})
})
}

View File

@ -90,7 +90,7 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"default": "Available",
"default": "Draft",
"fieldname": "status",
"fieldtype": "Select",
"hidden": 0,
@ -101,7 +101,7 @@
"label": "Status",
"length": 0,
"no_copy": 1,
"options": "Available\nSold\nScrapped",
"options": "Draft\nSubmitted\nPartially Depreciated\nFully Depreciated\nSold\nScrapped",
"permlevel": 0,
"precision": "",
"print_hide": 0,
@ -240,6 +240,32 @@
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "journal_entry_for_scrap",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Journal Entry for Scrap",
"length": 0,
"no_copy": 1,
"options": "Journal Entry",
"permlevel": 0,
"precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
@ -356,7 +382,7 @@
"in_list_view": 0,
"label": "Next Depreciation Date",
"length": 0,
"no_copy": 0,
"no_copy": 1,
"permlevel": 0,
"precision": "",
"print_hide": 0,
@ -458,7 +484,7 @@
"in_list_view": 0,
"label": "Current Value (After Depreciation)",
"length": 0,
"no_copy": 0,
"no_copy": 1,
"options": "Company:company:default_currency",
"permlevel": 0,
"precision": "",
@ -557,7 +583,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2016-03-09 12:22:05.223886",
"modified": "2016-03-11 12:23:37.114298",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Asset",

View File

@ -10,14 +10,20 @@ from frappe.model.document import Document
class Asset(Document):
def validate(self):
self.set_status()
self.validate_fixed_asset_item()
self.validate_asset_values()
self.set_depreciation_settings()
self.make_depreciation_schedule()
self.validate_depreciation_settings_in_company()
def on_submit(self):
self.set_status()
def on_cancel(self):
self.validate_cancellation()
self.delete_depreciation_entries()
self.set_status()
def validate_fixed_asset_item(self):
item = frappe.get_doc("Item", self.item_code)
@ -49,7 +55,7 @@ class Asset(Document):
def make_depreciation_schedule(self):
self.schedules = []
if not self.get("schedules") and self.status == "Available":
if not self.get("schedules"):
accumulated_depreciation = 0
value_after_depreciation = flt(self.current_value)
for n in xrange(self.number_of_depreciations):
@ -71,8 +77,8 @@ class Asset(Document):
depreciation_amount = (flt(self.current_value) -
flt(self.expected_value_after_useful_life)) / cint(self.number_of_depreciations)
else:
factor = 200 / cint(self.number_of_depreciations)
depreciation_amount = depreciable_value * factor / 100
factor = 200.0 / cint(self.number_of_depreciations)
depreciation_amount = flt(depreciable_value * factor / 100, 0)
value_after_depreciation = flt(depreciable_value) - depreciation_amount
if value_after_depreciation < flt(self.expected_value_after_useful_life):
@ -81,9 +87,11 @@ class Asset(Document):
return depreciation_amount
def validate_cancellation(self):
if self.status != "Available":
frappe.throw(_("Asset {0} cannot be cancelled, as it is already {1}")
.format(self.name, self.status))
if self.status not in ("Submitted", "Partially Depreciated", "Fully Depreciated"):
frappe.throw(_("Asset cannot be cancelled, as it is already {0}").format(self.status))
if self.purchase_invoice:
frappe.throw(_("Please cancel Purchase Invoice {0} first").format(self.purchase_invoice))
def delete_depreciation_entries(self):
total_depreciation_amount = 0
@ -94,4 +102,28 @@ class Asset(Document):
d.db_set("journal_entry", None)
total_depreciation_amount += flt(d.depreciation_amount)
self.db_set("current_value", (self.current_value - total_depreciation_amount))
def validate_depreciation_settings_in_company(self):
company = frappe.get_doc("Company", self.company)
for field in ("accumulated_depreciation_account", "depreciation_expense_account",
"disposal_account", "depreciation_cost_center"):
if not company.get(field):
frappe.throw(_("Please set {0} in Company {1}")
.format(company.meta.get_label(field), self.company))
def set_status(self, status=None):
if not status:
if self.docstatus == 0:
status = "Draft"
elif self.docstatus == 1:
status = "Submitted"
if self.journal_entry_for_scrap:
status = "Scrapped"
elif flt(self.current_value) <= flt(self.expected_value_after_useful_life):
status = "Fully Depreciated"
elif flt(self.current_value) < flt(self.gross_purchase_amount):
status = 'Partially Depreciated'
elif self.docstatus == 2:
status = "Cancelled"
frappe.db.set_value(self.doctype, self.name, "status", status)

View File

@ -17,7 +17,8 @@ def get_depreciable_assets(date):
return frappe.db.sql_list("""select a.name
from tabAsset a, `tabDepreciation Schedule` ds
where a.name = ds.parent and a.docstatus=1 and ds.schedule_date<=%s
and a.status = 'Available' and ifnull(ds.journal_entry, '')=''""", date)
and a.status in ('Submitted', 'Partially Depreciated')
and ifnull(ds.journal_entry, '')=''""", date)
def make_depreciation_entry(asset_name, date=None):
if not date:
@ -26,6 +27,8 @@ def make_depreciation_entry(asset_name, date=None):
asset = frappe.get_doc("Asset", asset_name)
fixed_asset_account, accumulated_depreciation_account, depreciation_expense_account = \
get_depreciation_accounts(asset)
depreciation_cost_center = frappe.db.get_value("Company", asset.company, "depreciation_cost_center")
for d in asset.get("schedules"):
if not d.journal_entry and getdate(d.schedule_date) <= getdate(date):
@ -34,7 +37,7 @@ def make_depreciation_entry(asset_name, date=None):
je.posting_date = d.schedule_date
je.company = asset.company
je.remark = "Depreciation Entry against {0} worth {1}".format(asset_name, d.depreciation_amount)
je.append("accounts", {
"account": accumulated_depreciation_account,
"credit_in_account_currency": d.depreciation_amount,
@ -46,15 +49,18 @@ def make_depreciation_entry(asset_name, date=None):
"account": depreciation_expense_account,
"debit_in_account_currency": d.depreciation_amount,
"reference_type": "Asset",
"reference_name": asset.name
"reference_name": asset.name,
"cost_center": depreciation_cost_center
})
je.flags.ignore_permissions = True
je.submit()
d.db_set("journal_entry", je.name)
asset.current_value -= d.depreciation_amount
frappe.db.set_value("Asset", asset_name, "current_value", asset.current_value)
asset.db_set("current_value", asset.current_value)
asset.set_status()
def get_depreciation_accounts(asset):
accounts = frappe.db.sql("""select fixed_asset_account, accumulated_depreciation_account,
@ -84,8 +90,10 @@ def get_depreciation_accounts(asset):
def scrap_asset(asset_name):
asset = frappe.get_doc("Asset", asset_name)
if asset.docstatus != 1 or asset.status != 'Available':
frappe.throw(_("Asset {0} must be submitted and available").format(asset.name))
if asset.docstatus != 1:
frappe.throw(_("Asset {0} must be submitted").format(asset.name))
elif asset.status in ("Cancelled", "Sold", "Scrapped"):
frappe.throw(_("Asset {0} cannot be scrapped, as it is already {1}").format(asset.name, asset.status))
je = frappe.new_doc("Journal Entry")
je.voucher_type = "Journal Entry"
@ -103,12 +111,23 @@ def scrap_asset(asset_name):
je.flags.ignore_permissions = True
je.submit()
frappe.db.set_value("Asset", asset_name, "status", "Scrapped")
frappe.db.set_value("Asset", asset_name, "journal_entry_for_scrap", je.name)
asset.set_status("Scrapped")
@frappe.whitelist()
def restore_asset(asset_name):
asset = frappe.get_doc("Asset", asset_name)
je = asset.journal_entry_for_scrap
asset.db_set("journal_entry_for_scrap", None)
frappe.get_doc("Journal Entry", je).cancel()
asset.set_status()
@frappe.whitelist()
def get_gl_entries_on_asset_disposal(asset, selling_amount=0):
fixed_asset_account, accumulated_depr_account, depr_expense_account = get_depreciation_accounts(asset)
disposal_account, disposal_cost_center = get_disposal_account_and_cost_center(asset.company)
disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(asset.company)
accumulated_depr_amount = flt(asset.gross_purchase_amount) - flt(asset.current_value)
gl_entries = [
@ -129,7 +148,7 @@ def get_gl_entries_on_asset_disposal(asset, selling_amount=0):
debit_or_credit = "debit" if profit_amount < 0 else "credit"
gl_entries.append({
"account": disposal_account,
"cost_center": disposal_cost_center,
"cost_center": depreciation_cost_center,
debit_or_credit: abs(profit_amount),
debit_or_credit + "_in_account_currency": abs(profit_amount)
})
@ -137,8 +156,12 @@ def get_gl_entries_on_asset_disposal(asset, selling_amount=0):
return gl_entries
def get_disposal_account_and_cost_center(company):
disposal_account, disposal_cost_center = frappe.db.get_value("Company", company,
["disposal_account", "disposal_cost_center"])
if not disposal_account or not disposal_cost_center:
frappe.throw(_("Please set 'Asset Disposal Account' and 'Asset Disposal Cost Center' in Company {0}").format(company))
return disposal_account, disposal_cost_center
disposal_account, depreciation_cost_center = frappe.db.get_value("Company", company,
["disposal_account", "depreciation_cost_center"])
if not disposal_account:
frappe.throw(_("Please set 'Asset Disposal Account' in Company {0}").format(company))
if not depreciation_cost_center:
frappe.throw(_("Please set 'Asset Depreciation Cost Center' in Company {0}").format(company))
return disposal_account, depreciation_cost_center

View File

@ -5,9 +5,13 @@ from __future__ import unicode_literals
import frappe
import unittest
from frappe.utils import cstr
from erpnext.accounts.doctype.asset.depreciation import post_depreciation_entries, scrap_asset, restore_asset
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
class TestAsset(unittest.TestCase):
def setUp(self):
set_depreciation_settings_in_company()
create_asset()
def test_fixed_asset_must_be_non_stock_item(self):
@ -15,11 +19,128 @@ class TestAsset(unittest.TestCase):
item.is_stock_item = 1
self.assertRaises(frappe.ValidationError, item.save)
def test_asset_purchase(self):
asset = create_asset()
def test_schedule_for_straight_line_method(self):
asset = frappe.get_doc("Asset", "Macbook Pro 1")
self.assertEqual(asset.current_value, 100000)
self.assertEqual(asset.status, "Draft")
expected_schedules = [
["2015-12-31", 30000, 30000],
["2016-03-31", 30000, 60000],
["2016-06-30", 30000, 90000]
]
schedules = [[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in asset.get("schedules")]
self.assertEqual(schedules, expected_schedules)
def test_schedule_for_double_declining_method(self):
asset = frappe.get_doc("Asset", "Macbook Pro 1")
asset.depreciation_method = "Double Declining Balance"
asset.save()
expected_schedules = [
["2015-12-31", 66667, 66667],
["2016-03-31", 22222, 88889],
["2016-06-30", 1111, 90000]
]
schedules = [[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in asset.get("schedules")]
self.assertEqual(schedules, expected_schedules)
def test_depreciation(self):
asset = frappe.get_doc("Asset", "Macbook Pro 1")
asset.submit()
asset.load_from_db()
self.assertEqual(asset.status, "Submitted")
post_depreciation_entries(date="2016-01-01")
asset.load_from_db()
self.assertEqual(asset.status, "Partially Depreciated")
expected_gle = (
("_Test Accumulated Depreciations - _TC", 0.0, 30000.0),
("_Test Depreciations - _TC", 30000.0, 0.0)
)
gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry`
where against_voucher_type='Asset' and against_voucher = %s
order by account""", asset.name)
self.assertEqual(gle, expected_gle)
self.assertEqual(asset.get("current_value"), 70000)
def test_scrap_asset(self):
asset = frappe.get_doc("Asset", "Macbook Pro 1")
asset.submit()
post_depreciation_entries(date="2016-01-01")
scrap_asset("Macbook Pro 1")
asset.load_from_db()
self.assertEqual(asset.status, "Scrapped")
self.assertTrue(asset.journal_entry_for_scrap)
expected_gle = (
("_Test Accumulated Depreciations - _TC", 30000.0, 0.0),
("_Test Fixed Asset - _TC", 0.0, 100000.0),
("_Test Gain/Loss on Asset Disposal - _TC", 70000.0, 0.0)
)
gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry`
where voucher_type='Journal Entry' and voucher_no = %s
order by account""", asset.journal_entry_for_scrap)
self.assertEqual(gle, expected_gle)
restore_asset("Macbook Pro 1")
asset.load_from_db()
self.assertFalse(asset.journal_entry_for_scrap)
self.assertEqual(asset.status, "Partially Depreciated")
def test_asset_sale(self):
frappe.get_doc("Asset", "Macbook Pro 1").submit()
post_depreciation_entries(date="2016-01-01")
si = create_sales_invoice(item_code="Macbook Pro", rate=25000, do_not_save=True)
si.get("items")[0].asset = "Macbook Pro 1"
si.submit()
self.assertEqual(frappe.db.get_value("Asset", "Macbook Pro 1", "status"), "Sold")
expected_gle = (
("_Test Accumulated Depreciations - _TC", 30000.0, 0.0),
("_Test Fixed Asset - _TC", 0.0, 100000.0),
("_Test Gain/Loss on Asset Disposal - _TC", 45000.0, 0.0),
("Debtors - _TC", 25000.0, 0.0)
)
gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry`
where voucher_type='Sales Invoice' and voucher_no = %s
order by account""", si.name)
self.assertEqual(gle, expected_gle)
si.cancel()
self.assertEqual(frappe.db.get_value("Asset", "Macbook Pro 1", "status"), "Partially Depreciated")
def tearDown(self):
asset = frappe.get_doc("Asset", "Macbook Pro 1")
if asset.docstatus == 1 and asset.status not in ("Scrapped", "Sold", "Draft", "Cancelled"):
asset.cancel()
self.assertEqual(frappe.db.get_value("Asset", "Macbook Pro 1", "status"), "Cancelled")
frappe.delete_doc("Asset", "Macbook Pro 1")
def create_asset():
if not frappe.db.exists("Asset Category", "Computers"):
@ -33,6 +154,7 @@ def create_asset():
"asset_name": "Macbook Pro 1",
"asset_category": "Computers",
"item_code": "Macbook Pro",
"company": "_Test Company",
"purchase_date": "2015-01-01",
"next_depreciation_date": "2015-12-31",
"gross_purchase_amount": 100000,
@ -48,8 +170,8 @@ def create_asset():
def create_asset_category():
asset_category = frappe.new_doc("Asset Category")
asset_category.asset_category_name = "Computers"
asset_category.number_of_depreciations = 5
asset_category.number_of_months_in_a_period = 12
asset_category.number_of_depreciations = 3
asset_category.number_of_months_in_a_period = 3
asset_category.append("accounts", {
"company": "_Test Company",
"fixed_asset_account": "_Test Fixed Asset - _TC",
@ -73,4 +195,10 @@ def create_fixed_asset_item():
except frappe.DuplicateEntryError:
pass
def set_depreciation_settings_in_company():
company = frappe.get_doc("Company", "_Test Company")
company.accumulated_depreciation_account = "_Test Accumulated Depreciations - _TC"
company.depreciation_expense_account = "_Test Depreciations - _TC"
company.disposal_account = "_Test Gain/Loss on Asset Disposal - _TC"
company.depreciation_cost_center = "_Test Cost Center - _TC"
company.save()

View File

@ -18,7 +18,7 @@ class TestAssetCategory(unittest.TestCase):
asset_category.append("accounts", {
"company": "_Test Company",
"fixed_asset_account": "_Test Fixed Asset - _TC",
"accumulated_depreciation_account": "_Test Accoumulated Depreciations - _TC",
"accumulated_depreciation_account": "_Test Accumulated Depreciations - _TC",
"depreciation_expense_account": "_Test Depreciations - _TC"
})

View File

@ -88,8 +88,8 @@ class GLEntry(Document):
"Cost Center", self.cost_center, "company")
return self.cost_center_company[self.cost_center]
if self.cost_center and _get_cost_center_company() != self.company:
if self.cost_center and _get_cost_center_company() != self.company:
frappe.throw(_("Cost Center {0} does not belong to Company {1}").format(self.cost_center, self.company))
def validate_party(self):

View File

@ -217,7 +217,7 @@ cur_frm.set_query("asset", "items", function(doc, cdt, cdn) {
'item_code': d.item_code,
'docstatus': 1,
'company': doc.company,
'status': 'Available'
'status': 'Submitted'
}
}
});

View File

@ -237,7 +237,7 @@ class PurchaseInvoice(BuyingController):
def on_submit(self):
self.check_prev_docstatus()
self.post_asset_depreciation()
self.validate_asset()
frappe.get_doc('Authorization Control').validate_approving_authority(self.doctype,
self.company, self.base_grand_total)
@ -252,29 +252,30 @@ class PurchaseInvoice(BuyingController):
self.update_project()
def post_asset_depreciation(self):
def validate_asset(self):
for d in self.get("items"):
if frappe.db.get_value("Item", d.item_code, "is_fixed_asset"):
if not d.asset:
frappe.throw(_("Row #{0}: Asset is mandatory against a Fixed Asset Item").format(d.idx))
else:
asset = frappe.get_doc("Asset", d.asset)
self.validate_asset(asset, d)
super(PurchaseInvoice, self).validate_asset(asset, d)
if getdate(asset.purchase_date) != getdate(self.posting_date):
frappe.throw(_("Purchase Date of asset {0} does not match with Purchase Invoice date")
.format(d.asset))
if asset.supplier != self.supplier:
frappe.throw(_("Supplier of asset {0} does not match with the supplier in the Purchase Invoice").format(d.asset))
if asset.status != "Available":
frappe.throw(_("Row #{0}: Asset {1} is already {2}")
.format(d.idx, d.asset, asset.status))
frappe.db.set_value("Asset", asset.name, "purchase_invoice",
(self.name if self.docstatus==1 else None))
def validate_asset(self, asset, item_row):
super(PurchaseInvoice, self).validate_asset(asset, item_row)
if getdate(asset.purchase_date) != getdate(self.posting_date):
frappe.throw(_("Purchase Date of asset {0} does not match with Purchase Invoice date")
.format(item_row.asset))
if asset.supplier != self.supplier:
frappe.throw(_("Supplier of asset {0} does not match with the supplier in the Purchase Invoice")
.format(item_row.asset))
def make_gl_entries(self):
auto_accounting_for_stock = \
cint(frappe.defaults.get_global_default("auto_accounting_for_stock"))
@ -441,7 +442,7 @@ class PurchaseInvoice(BuyingController):
self.make_gl_entries_on_cancel()
self.update_project()
self.post_asset_depreciation()
self.validate_asset()
def update_project(self):
project_list = []

View File

@ -475,11 +475,11 @@ cur_frm.set_query("debit_to", function(doc) {
cur_frm.set_query("asset", "items", function(doc, cdt, cdn) {
var d = locals[cdt][cdn];
return {
filters: {
'item_code': d.item_code,
'docstatus': 1,
'company': doc.company,
'status': 'Available'
}
filters: [
["Asset", "item_code", "=", d.item_code],
["Asset", "docstatus", "=", 1],
["Asset", "status", "in", ["Submitted", "Partially Depreciated", "Fully Depreciated"]],
["Asset", "company", "=", doc.company]
]
}
});

View File

@ -484,6 +484,13 @@ class SalesInvoice(SellingController):
if d.delivery_note and frappe.db.get_value("Delivery Note", d.delivery_note, "docstatus") != 1:
throw(_("Delivery Note {0} is not submitted").format(d.delivery_note))
def validate_asset(self, asset, item_row):
super(SalesInvoice, self).validate_asset(asset, item_row)
if self.docstatus == 1 and asset.status in ("Scrapped", "Cancelled", "Sold"):
frappe.throw(_("Row #{0}: Asset {1} cannot be submitted, it is already {2}")
.format(item_row.idx, asset.name, asset.status))
def make_gl_entries(self, repost_future_gle=True):
gl_entries = self.get_gl_entries()
@ -582,7 +589,7 @@ class SalesInvoice(SellingController):
for gle in fixed_asset_gl_entries:
gl_entries.append(self.get_gl_dict(gle))
frappe.db.set_value("Asset", asset.name, "status", "Sold")
asset.set_status("Sold" if self.docstatus==1 else None)
else:
account_currency = get_account_currency(item.income_account)
gl_entries.append(

View File

@ -141,7 +141,7 @@ erpnext.company.setup_queries = function(frm) {
["disposal_account", {"report_type": "Profit and Loss"}],
["cost_center", {}],
["round_off_cost_center", {}],
["disposal_cost_center", {}]
["depreciation_cost_center", {}]
], function(i, v) {
erpnext.company.set_custom_query(frm, v);
});

View File

@ -1091,14 +1091,14 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "disposal_cost_center",
"fieldname": "depreciation_cost_center",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Asset Disposal Cost Center",
"label": "Asset Depreciation Cost Center",
"length": 0,
"no_copy": 1,
"options": "Cost Center",
@ -1387,7 +1387,7 @@
"istable": 0,
"max_attachments": 0,
"menu_index": 0,
"modified": "2016-03-09 12:06:12.189968",
"modified": "2016-03-10 04:34:43.440914",
"modified_by": "Administrator",
"module": "Setup",
"name": "Company",
@ -1536,6 +1536,5 @@
],
"read_only": 0,
"read_only_onload": 0,
"sort_order": "ASC",
"version": 0
"sort_order": "ASC"
}