diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 1f5879d7bff..608f0828fee 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -7,17 +7,7 @@ from frappe import _, msgprint, throw from frappe.contacts.doctype.address.address import get_address_display from frappe.model.mapper import get_mapped_doc from frappe.model.utils import get_fetch_values -from frappe.utils import ( - add_days, - add_months, - cint, - cstr, - flt, - formatdate, - get_link_to_form, - getdate, - nowdate, -) +from frappe.utils import add_days, cint, cstr, flt, formatdate, get_link_to_form, getdate, nowdate import erpnext from erpnext.accounts.deferred_revenue import validate_service_stop_date @@ -35,7 +25,6 @@ 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, - make_depreciation_entry, ) from erpnext.controllers.accounts_controller import validate_account_head from erpnext.controllers.selling_controller import SellingController @@ -186,7 +175,7 @@ class SalesInvoice(SellingController): if self.update_stock: frappe.throw(_("'Update Stock' cannot be checked for fixed asset sale")) - elif asset.status in ("Scrapped", "Cancelled") or ( + elif asset.status in ("Scrapped", "Cancelled", "Capitalized", "Decapitalized") or ( asset.status == "Sold" and not self.is_return ): frappe.throw( @@ -1097,7 +1086,7 @@ class SalesInvoice(SellingController): asset.db_set("disposal_date", None) if asset.calculate_depreciation: - self.reverse_depreciation_entry_made_after_sale(asset) + self.reverse_depreciation_entry_made_after_disposal(asset) self.reset_depreciation_schedule(asset) else: @@ -1162,101 +1151,6 @@ class SalesInvoice(SellingController): 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(date_of_sale=self.posting_date) - asset.save() - - make_depreciation_entry(asset.name, 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(date_of_return=self.posting_date) - - self.modify_depreciation_schedule_for_asset_repairs(asset) - asset.save() - - 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 reverse_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 - ) or self.sale_happens_in_the_future(posting_date_of_original_invoice): - - reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry) - reverse_journal_entry.posting_date = nowdate() - frappe.flags.is_reverse_depr_entry = True - reverse_journal_entry.submit() - - frappe.flags.is_reverse_depr_entry = False - asset.flags.ignore_validate_update_after_submit = True - schedule.journal_entry = None - depreciation_amount = self.get_depreciation_amount_in_je(reverse_journal_entry) - asset.finance_books[0].value_after_depreciation += depreciation_amount - asset.save() - - 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 sale_happens_in_the_future(self, posting_date_of_original_invoice): - if posting_date_of_original_invoice > getdate(): - return True - - return False - - def get_depreciation_amount_in_je(self, 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 - @property def enable_discount_accounting(self): if not hasattr(self, "_enable_discount_accounting"): diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index a22d70dd630..8ac7ed6387b 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -79,12 +79,12 @@ class Asset(AccountsController): _("Purchase Invoice cannot be made against an existing asset {0}").format(self.name) ) - def prepare_depreciation_data(self, date_of_sale=None, date_of_return=None): + def prepare_depreciation_data(self, date_of_disposal=None, date_of_return=None): if self.calculate_depreciation: self.value_after_depreciation = 0 self.set_depreciation_rate() - self.make_depreciation_schedule(date_of_sale) - self.set_accumulated_depreciation(date_of_sale, date_of_return) + self.make_depreciation_schedule(date_of_disposal) + self.set_accumulated_depreciation(date_of_disposal, date_of_return) else: self.finance_books = [] self.value_after_depreciation = flt(self.gross_purchase_amount) - flt( @@ -223,7 +223,7 @@ class Asset(AccountsController): self.get_depreciation_rate(d, on_validate=True), d.precision("rate_of_depreciation") ) - def make_depreciation_schedule(self, date_of_sale): + def make_depreciation_schedule(self, date_of_disposal): if "Manual" not in [d.depreciation_method for d in self.finance_books] and not self.get( "schedules" ): @@ -235,9 +235,9 @@ class Asset(AccountsController): start = self.clear_depreciation_schedule() for finance_book in self.get("finance_books"): - self._make_depreciation_schedule(finance_book, start, date_of_sale) + self._make_depreciation_schedule(finance_book, start, date_of_disposal) - def _make_depreciation_schedule(self, finance_book, start, date_of_sale): + def _make_depreciation_schedule(self, finance_book, start, date_of_disposal): self.validate_asset_finance_books(finance_book) value_after_depreciation = self._get_value_after_depreciation(finance_book) @@ -274,15 +274,15 @@ class Asset(AccountsController): monthly_schedule_date = add_months(schedule_date, -finance_book.frequency_of_depreciation + 1) # if asset is being sold - if date_of_sale: + if date_of_disposal: from_date = self.get_from_date(finance_book.finance_book) depreciation_amount, days, months = self.get_pro_rata_amt( - finance_book, depreciation_amount, from_date, date_of_sale + finance_book, depreciation_amount, from_date, date_of_disposal ) if depreciation_amount > 0: self._add_depreciation_row( - date_of_sale, + date_of_disposal, depreciation_amount, finance_book.depreciation_method, finance_book.finance_book, diff --git a/erpnext/assets/doctype/asset/asset_list.js b/erpnext/assets/doctype/asset/asset_list.js index 4302cb2c518..3d00eb74aa0 100644 --- a/erpnext/assets/doctype/asset/asset_list.js +++ b/erpnext/assets/doctype/asset/asset_list.js @@ -10,6 +10,9 @@ frappe.listview_settings['Asset'] = { } else if (doc.status === "Sold") { return [__("Sold"), "green", "status,=,Sold"]; + } else if (["Capitalized", "Decapitalized"].includes(doc.status)) { + return [__(doc.status), "grey", "status,=," + doc.status]; + } else if (doc.status === "Scrapped") { return [__("Scrapped"), "grey", "status,=,Scrapped"]; diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index 3f7e9459943..74386384c5d 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -11,7 +11,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( ) -def post_depreciation_entries(date=None): +def post_depreciation_entries(date=None, commit=True): # Return if automatic booking of asset depreciation is disabled if not cint( frappe.db.get_value("Accounts Settings", None, "book_asset_depreciation_entry_automatically") @@ -22,7 +22,8 @@ def post_depreciation_entries(date=None): date = today() for asset in get_depreciable_assets(date): make_depreciation_entry(asset, date) - frappe.db.commit() + if commit: + frappe.db.commit() def get_depreciable_assets(date): @@ -190,7 +191,7 @@ def scrap_asset(asset_name): if asset.docstatus != 1: frappe.throw(_("Asset {0} must be submitted").format(asset.name)) - elif asset.status in ("Cancelled", "Sold", "Scrapped"): + elif asset.status in ("Cancelled", "Sold", "Scrapped", "Capitalized", "Decapitalized"): frappe.throw( _("Asset {0} cannot be scrapped, as it is already {1}").format(asset.name, asset.status) ) @@ -358,3 +359,30 @@ def get_disposal_account_and_cost_center(company): frappe.throw(_("Please set 'Asset Depreciation Cost Center' in Company {0}").format(company)) return disposal_account, depreciation_cost_center + + +@frappe.whitelist() +def get_value_after_depreciation_on_disposal_date(asset, disposal_date, finance_book=None): + asset_doc = frappe.get_doc("Asset", asset) + + if asset_doc.calculate_depreciation: + asset_doc.prepare_depreciation_data(getdate(disposal_date)) + + finance_book_id = 1 + if finance_book: + for fb in asset_doc.finance_books: + if fb.finance_book == finance_book: + finance_book_id = fb.idx + break + + asset_schedules = [ + sch for sch in asset_doc.schedules if cint(sch.finance_book_id) == finance_book_id + ] + accumulated_depr_amount = asset_schedules[-1].accumulated_depreciation_amount + + return flt( + flt(asset_doc.gross_purchase_amount) - accumulated_depr_amount, + asset_doc.precision("gross_purchase_amount"), + ) + else: + return flt(asset_doc.value_after_depreciation) diff --git a/erpnext/assets/doctype/asset_capitalization/__init__.py b/erpnext/assets/doctype/asset_capitalization/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js new file mode 100644 index 00000000000..d135e60ae7f --- /dev/null +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js @@ -0,0 +1,414 @@ +// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.provide("erpnext.assets"); + + +erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.stock.StockController { + setup() { + this.setup_posting_date_time_check(); + } + + onload() { + this.setup_queries(); + } + + refresh() { + erpnext.hide_company(); + this.show_general_ledger(); + if ((this.frm.doc.stock_items && this.frm.doc.stock_items.length) || !this.frm.doc.target_is_fixed_asset) { + this.show_stock_ledger(); + } + } + + setup_queries() { + var me = this; + + me.setup_warehouse_query(); + + me.frm.set_query("target_item_code", function() { + return erpnext.queries.item(); + }); + + me.frm.set_query("target_asset", function() { + var filters = {}; + + if (me.frm.doc.target_item_code) { + filters['item_code'] = me.frm.doc.target_item_code; + } + + filters['status'] = ["not in", ["Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"]]; + filters['docstatus'] = 1; + + return { + filters: filters + }; + }); + + me.frm.set_query("asset", "asset_items", function() { + var filters = { + 'status': ["not in", ["Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"]], + 'docstatus': 1 + }; + + if (me.frm.doc.target_asset) { + filters['name'] = ['!=', me.frm.doc.target_asset]; + } + + return { + filters: filters + }; + }); + + me.frm.set_query("item_code", "stock_items", function() { + return erpnext.queries.item({"is_stock_item": 1}); + }); + + me.frm.set_query("item_code", "service_items", function() { + return erpnext.queries.item({"is_stock_item": 0, "is_fixed_asset": 0}); + }); + + me.frm.set_query('batch_no', 'stock_items', function(doc, cdt, cdn) { + var item = locals[cdt][cdn]; + if (!item.item_code) { + frappe.throw(__("Please enter Item Code to get Batch Number")); + } else { + var filters = { + 'item_code': item.item_code, + 'posting_date': me.frm.doc.posting_date || frappe.datetime.nowdate(), + 'warehouse': item.warehouse + }; + + return { + query: "erpnext.controllers.queries.get_batch_no", + filters: filters + }; + } + }); + + me.frm.set_query('expense_account', 'service_items', function() { + return { + filters: { + "account_type": ['in', ["Tax", "Expense Account", "Income Account", "Expenses Included In Valuation", "Expenses Included In Asset Valuation"]], + "is_group": 0, + "company": me.frm.doc.company + } + }; + }); + } + + target_item_code() { + return this.get_target_item_details(); + } + + target_asset() { + return this.get_target_asset_details(); + } + + item_code(doc, cdt, cdn) { + var row = frappe.get_doc(cdt, cdn); + if (cdt === "Asset Capitalization Stock Item") { + this.get_consumed_stock_item_details(row); + } else if (cdt == "Asset Capitalization Service Item") { + this.get_service_item_details(row); + } + } + + warehouse(doc, cdt, cdn) { + var row = frappe.get_doc(cdt, cdn); + if (cdt === "Asset Capitalization Stock Item") { + this.get_warehouse_details(row); + } + } + + asset(doc, cdt, cdn) { + var row = frappe.get_doc(cdt, cdn); + if (cdt === "Asset Capitalization Asset Item") { + this.get_consumed_asset_details(row); + } + } + + posting_date() { + if (this.frm.doc.posting_date) { + frappe.run_serially([ + () => this.get_all_item_warehouse_details(), + () => this.get_all_asset_values() + ]); + } + } + + posting_time() { + if (this.frm.doc.posting_time) { + this.get_all_item_warehouse_details(); + } + } + + finance_book(doc, cdt, cdn) { + if (cdt === "Asset Capitalization Asset Item") { + var row = frappe.get_doc(cdt, cdn); + this.get_consumed_asset_details(row); + } else { + this.get_all_asset_values(); + } + } + + stock_qty() { + this.calculate_totals(); + } + + qty() { + this.calculate_totals(); + } + + target_qty() { + this.calculate_totals(); + } + + rate() { + this.calculate_totals(); + } + + company() { + var me = this; + + if (me.frm.doc.company) { + frappe.model.set_value(me.frm.doc.doctype, me.frm.doc.name, "cost_center", null); + $.each(me.frm.doc.stock_items || [], function (i, d) { + frappe.model.set_value(d.doctype, d.name, "cost_center", null); + }); + $.each(me.frm.doc.asset_items || [], function (i, d) { + frappe.model.set_value(d.doctype, d.name, "cost_center", null); + }); + $.each(me.frm.doc.service_items || [], function (i, d) { + frappe.model.set_value(d.doctype, d.name, "cost_center", null); + }); + } + + erpnext.accounts.dimensions.update_dimension(me.frm, me.frm.doctype); + } + + stock_items_add(doc, cdt, cdn) { + erpnext.accounts.dimensions.copy_dimension_from_first_row(this.frm, cdt, cdn, 'stock_items'); + } + + asset_items_add(doc, cdt, cdn) { + erpnext.accounts.dimensions.copy_dimension_from_first_row(this.frm, cdt, cdn, 'asset_items'); + } + + serivce_items_add(doc, cdt, cdn) { + erpnext.accounts.dimensions.copy_dimension_from_first_row(this.frm, cdt, cdn, 'service_items'); + } + + get_target_item_details() { + var me = this; + + if (me.frm.doc.target_item_code) { + return me.frm.call({ + method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_target_item_details", + child: me.frm.doc, + args: { + item_code: me.frm.doc.target_item_code, + company: me.frm.doc.company, + }, + callback: function (r) { + if (!r.exc) { + me.frm.refresh_fields(); + } + } + }); + } + } + + get_target_asset_details() { + var me = this; + + if (me.frm.doc.target_asset) { + return me.frm.call({ + method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_target_asset_details", + child: me.frm.doc, + args: { + asset: me.frm.doc.target_asset, + company: me.frm.doc.company, + }, + callback: function (r) { + if (!r.exc) { + me.frm.refresh_fields(); + } + } + }); + } + } + + get_consumed_stock_item_details(row) { + var me = this; + + if (row && row.item_code) { + return me.frm.call({ + method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_consumed_stock_item_details", + child: row, + args: { + args: { + item_code: row.item_code, + warehouse: row.warehouse, + stock_qty: flt(row.stock_qty), + doctype: me.frm.doc.doctype, + name: me.frm.doc.name, + company: me.frm.doc.company, + posting_date: me.frm.doc.posting_date, + posting_time: me.frm.doc.posting_time, + } + }, + callback: function (r) { + if (!r.exc) { + me.calculate_totals(); + } + } + }); + } + } + + get_consumed_asset_details(row) { + var me = this; + + if (row && row.asset) { + return me.frm.call({ + method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_consumed_asset_details", + child: row, + args: { + args: { + asset: row.asset, + doctype: me.frm.doc.doctype, + name: me.frm.doc.name, + company: me.frm.doc.company, + finance_book: row.finance_book || me.frm.doc.finance_book, + posting_date: me.frm.doc.posting_date, + posting_time: me.frm.doc.posting_time, + } + }, + callback: function (r) { + if (!r.exc) { + me.calculate_totals(); + } + } + }); + } + } + + get_service_item_details(row) { + var me = this; + + if (row && row.item_code) { + return me.frm.call({ + method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_service_item_details", + child: row, + args: { + args: { + item_code: row.item_code, + qty: flt(row.qty), + expense_account: row.expense_account, + company: me.frm.doc.company, + } + }, + callback: function (r) { + if (!r.exc) { + me.calculate_totals(); + } + } + }); + } + } + + get_warehouse_details(item) { + var me = this; + if (item.item_code && item.warehouse) { + me.frm.call({ + method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_warehouse_details", + child: item, + args: { + args: { + 'item_code': item.item_code, + 'warehouse': cstr(item.warehouse), + 'qty': flt(item.stock_qty), + 'serial_no': item.serial_no, + 'posting_date': me.frm.doc.posting_date, + 'posting_time': me.frm.doc.posting_time, + 'company': me.frm.doc.company, + 'voucher_type': me.frm.doc.doctype, + 'voucher_no': me.frm.doc.name, + 'allow_zero_valuation': 1 + } + }, + callback: function(r) { + if (!r.exc) { + me.calculate_totals(); + } + } + }); + } + } + + get_all_item_warehouse_details() { + var me = this; + return me.frm.call({ + method: "set_warehouse_details", + doc: me.frm.doc, + callback: function(r) { + if (!r.exc) { + me.calculate_totals(); + } + } + }); + } + + get_all_asset_values() { + var me = this; + return me.frm.call({ + method: "set_asset_values", + doc: me.frm.doc, + callback: function(r) { + if (!r.exc) { + me.calculate_totals(); + } + } + }); + } + + calculate_totals() { + var me = this; + + me.frm.doc.stock_items_total = 0; + me.frm.doc.asset_items_total = 0; + me.frm.doc.service_items_total = 0; + + $.each(me.frm.doc.stock_items || [], function (i, d) { + d.amount = flt(flt(d.stock_qty) * flt(d.valuation_rate), precision('amount', d)); + me.frm.doc.stock_items_total += d.amount; + }); + + $.each(me.frm.doc.asset_items || [], function (i, d) { + d.asset_value = flt(flt(d.asset_value), precision('asset_value', d)); + me.frm.doc.asset_items_total += d.asset_value; + }); + + $.each(me.frm.doc.service_items || [], function (i, d) { + d.amount = flt(flt(d.qty) * flt(d.rate), precision('amount', d)); + me.frm.doc.service_items_total += d.amount; + }); + + me.frm.doc.stock_items_total = flt(me.frm.doc.stock_items_total, precision('stock_items_total')); + me.frm.doc.asset_items_total = flt(me.frm.doc.asset_items_total, precision('asset_items_total')); + me.frm.doc.service_items_total = flt(me.frm.doc.service_items_total, precision('service_items_total')); + + me.frm.doc.total_value = me.frm.doc.stock_items_total + me.frm.doc.asset_items_total + me.frm.doc.service_items_total; + me.frm.doc.total_value = flt(me.frm.doc.total_value, precision('total_value')); + + me.frm.doc.target_qty = flt(me.frm.doc.target_qty, precision('target_qty')); + me.frm.doc.target_incoming_rate = me.frm.doc.target_qty ? me.frm.doc.total_value / flt(me.frm.doc.target_qty) + : me.frm.doc.total_value; + + me.frm.refresh_fields(); + } +}; + +//$.extend(cur_frm.cscript, new erpnext.assets.AssetCapitalization({frm: cur_frm})); +cur_frm.cscript = new erpnext.assets.AssetCapitalization({frm: cur_frm}); diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json new file mode 100644 index 00000000000..d7e6b547162 --- /dev/null +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json @@ -0,0 +1,383 @@ +{ + "actions": [], + "autoname": "naming_series:", + "creation": "2021-09-04 13:38:04.217187", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "title", + "target_item_code", + "target_item_name", + "target_is_fixed_asset", + "target_has_batch_no", + "target_has_serial_no", + "entry_type", + "finance_book", + "naming_series", + "column_break_9", + "company", + "posting_date", + "posting_time", + "set_posting_time", + "amended_from", + "target_item_details_section", + "target_asset", + "target_asset_name", + "target_warehouse", + "target_batch_no", + "target_serial_no", + "column_break_5", + "target_qty", + "target_stock_uom", + "section_break_16", + "stock_items", + "stock_items_total", + "section_break_26", + "asset_items", + "asset_items_total", + "service_expenses_section", + "service_items", + "service_items_total", + "totals_section", + "total_value", + "column_break_36", + "target_incoming_rate", + "accounting_dimensions_section", + "cost_center", + "dimension_col_break", + "target_fixed_asset_account" + ], + "fields": [ + { + "fieldname": "title", + "fieldtype": "Data", + "hidden": 1, + "label": "Title" + }, + { + "fieldname": "target_item_code", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Target Item Code", + "options": "Item", + "reqd": 1 + }, + { + "depends_on": "eval:doc.target_item_code && doc.target_item_name != doc.target_item_code", + "fetch_from": "target_item_code.item_name", + "fieldname": "target_item_name", + "fieldtype": "Data", + "label": "Target Item Name", + "read_only": 1 + }, + { + "default": "0", + "fetch_from": "target_item_code.is_fixed_asset", + "fieldname": "target_is_fixed_asset", + "fieldtype": "Check", + "hidden": 1, + "label": "Target Is Fixed Asset", + "read_only": 1 + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval:!doc.target_item_code || doc.target_is_fixed_asset", + "fieldname": "target_asset", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Target Asset", + "no_copy": 1, + "options": "Asset" + }, + { + "depends_on": "target_asset", + "fetch_from": "target_asset.asset_name", + "fieldname": "target_asset_name", + "fieldtype": "Data", + "label": "Asset Name", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "fetch_from": "asset.company", + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "default": "Today", + "fieldname": "posting_date", + "fieldtype": "Date", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Posting Date", + "no_copy": 1, + "reqd": 1, + "search_index": 1 + }, + { + "default": "Now", + "fieldname": "posting_time", + "fieldtype": "Time", + "label": "Posting Time", + "no_copy": 1, + "reqd": 1 + }, + { + "default": "0", + "depends_on": "eval:doc.docstatus==0", + "fieldname": "set_posting_time", + "fieldtype": "Check", + "label": "Edit Posting Date and Time" + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Series", + "options": "ACC-ASC-.YYYY.-", + "reqd": 1 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Asset Capitalization", + "print_hide": 1, + "read_only": 1 + }, + { + "depends_on": "eval:doc.docstatus == 0 || (doc.stock_items && doc.stock_items.length)", + "fieldname": "section_break_16", + "fieldtype": "Section Break", + "label": "Consumed Stock Items" + }, + { + "fieldname": "stock_items", + "fieldtype": "Table", + "label": "Stock Items", + "options": "Asset Capitalization Stock Item" + }, + { + "fieldname": "target_item_details_section", + "fieldtype": "Section Break", + "label": "Target Item Details" + }, + { + "depends_on": "eval:!doc.target_is_fixed_asset", + "fieldname": "target_warehouse", + "fieldtype": "Link", + "label": "Target Warehouse", + "options": "Warehouse" + }, + { + "depends_on": "target_has_batch_no", + "fieldname": "target_batch_no", + "fieldtype": "Link", + "label": "Target Batch No", + "options": "Batch" + }, + { + "default": "1", + "fieldname": "target_qty", + "fieldtype": "Float", + "label": "Target Qty", + "read_only_depends_on": "target_is_fixed_asset" + }, + { + "fetch_from": "target_item_code.stock_uom", + "fieldname": "target_stock_uom", + "fieldtype": "Link", + "label": "Stock UOM", + "options": "UOM", + "read_only": 1 + }, + { + "default": "0", + "fetch_from": "target_item_code.has_batch_no", + "fieldname": "target_has_batch_no", + "fieldtype": "Check", + "hidden": 1, + "label": "Target Has Batch No", + "read_only": 1 + }, + { + "default": "0", + "fetch_from": "target_item_code.has_serial_no", + "fieldname": "target_has_serial_no", + "fieldtype": "Check", + "hidden": 1, + "label": "Target Has Serial No", + "read_only": 1 + }, + { + "depends_on": "target_has_serial_no", + "fieldname": "target_serial_no", + "fieldtype": "Small Text", + "label": "Target Serial No" + }, + { + "depends_on": "eval:doc.docstatus == 0 || (doc.asset_items && doc.asset_items.length)", + "fieldname": "section_break_26", + "fieldtype": "Section Break", + "label": "Consumed Asset Items" + }, + { + "fieldname": "asset_items", + "fieldtype": "Table", + "label": "Assets", + "options": "Asset Capitalization Asset Item" + }, + { + "fieldname": "entry_type", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Entry Type", + "options": "\nCapitalization\nDecapitalization", + "read_only": 1 + }, + { + "fieldname": "stock_items_total", + "fieldtype": "Currency", + "label": "Consumed Stock Total Value", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "asset_items_total", + "fieldtype": "Currency", + "label": "Consumed Asset Total Value", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "finance_book", + "fieldtype": "Link", + "label": "Finance Book", + "options": "Finance Book" + }, + { + "depends_on": "eval:doc.docstatus == 0 || (doc.service_items && doc.service_items.length)", + "fieldname": "service_expenses_section", + "fieldtype": "Section Break", + "label": "Service Expenses" + }, + { + "fieldname": "service_items", + "fieldtype": "Table", + "label": "Services", + "options": "Asset Capitalization Service Item" + }, + { + "fieldname": "service_items_total", + "fieldtype": "Currency", + "label": "Service Expense Total Amount", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "totals_section", + "fieldtype": "Section Break", + "label": "Totals" + }, + { + "fieldname": "total_value", + "fieldtype": "Currency", + "label": "Total Value", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "column_break_36", + "fieldtype": "Column Break" + }, + { + "fieldname": "target_incoming_rate", + "fieldtype": "Currency", + "label": "Target Incoming Rate", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" + }, + { + "fieldname": "target_fixed_asset_account", + "fieldtype": "Link", + "label": "Target Fixed Asset Account", + "options": "Account", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2021-09-15 15:41:27.917458", + "modified_by": "Administrator", + "module": "Assets", + "name": "Asset Capitalization", + "naming_rule": "By \"Naming Series\" field", + "owner": "Administrator", + "permissions": [ + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Manufacturing Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Quality Manager", + "share": 1, + "submit": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "title", + "track_changes": 1, + "track_seen": 1 +} \ No newline at end of file diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py new file mode 100644 index 00000000000..a8f2d79c270 --- /dev/null +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -0,0 +1,663 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import json + +import frappe + +# import erpnext +from frappe import _ +from frappe.utils import cint, flt +from six import string_types + +from erpnext.assets.doctype.asset.depreciation import ( + get_gl_entries_on_asset_disposal, + get_gl_entries_on_asset_regain, + get_value_after_depreciation_on_disposal_date, +) +from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account +from erpnext.assets.doctype.asset_value_adjustment.asset_value_adjustment import ( + get_current_asset_value, +) +from erpnext.controllers.stock_controller import StockController +from erpnext.setup.doctype.brand.brand import get_brand_defaults +from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults +from erpnext.stock import get_warehouse_account_map +from erpnext.stock.doctype.item.item import get_item_defaults +from erpnext.stock.get_item_details import ( + get_default_cost_center, + get_default_expense_account, + get_item_warehouse, +) +from erpnext.stock.stock_ledger import get_previous_sle +from erpnext.stock.utils import get_incoming_rate + +force_fields = ['target_item_name', 'target_asset_name', 'item_name', 'asset_name', + 'target_is_fixed_asset', 'target_has_serial_no', 'target_has_batch_no', + 'target_stock_uom', 'stock_uom', 'target_fixed_asset_account', 'fixed_asset_account'] + + +class AssetCapitalization(StockController): + def validate(self): + self.validate_posting_time() + self.set_missing_values(for_validate=True) + self.set_entry_type() + self.validate_target_item() + self.validate_target_asset() + self.validate_consumed_stock_item() + self.validate_consumed_asset_item() + self.validate_service_item() + self.set_warehouse_details() + self.set_asset_values() + self.calculate_totals() + self.set_title() + + def before_submit(self): + self.validate_source_mandatory() + + def on_submit(self): + self.update_stock_ledger() + self.make_gl_entries() + + def on_cancel(self): + self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation') + self.update_stock_ledger() + self.make_gl_entries() + + def set_entry_type(self): + self.entry_type = "Capitalization" if self.target_is_fixed_asset else "Decapitalization" + + def set_title(self): + self.title = self.target_asset_name or self.target_item_name or self.target_item_code + + def set_missing_values(self, for_validate=False): + target_item_details = get_target_item_details(self.target_item_code, self.company) + for k, v in target_item_details.items(): + if self.meta.has_field(k) and (not self.get(k) or k in force_fields): + self.set(k, v) + + # Remove asset if item not a fixed asset + if not self.target_is_fixed_asset: + self.target_asset = None + + target_asset_details = get_target_asset_details(self.target_asset, self.company) + for k, v in target_asset_details.items(): + if self.meta.has_field(k) and (not self.get(k) or k in force_fields): + self.set(k, v) + + for d in self.stock_items: + args = self.as_dict() + args.update(d.as_dict()) + args.doctype = self.doctype + args.name = self.name + consumed_stock_item_details = get_consumed_stock_item_details(args, get_valuation_rate=False) + for k, v in consumed_stock_item_details.items(): + if d.meta.has_field(k) and (not d.get(k) or k in force_fields): + d.set(k, v) + + for d in self.asset_items: + args = self.as_dict() + args.update(d.as_dict()) + args.doctype = self.doctype + args.name = self.name + args.finance_book = d.get('finance_book') or self.get('finance_book') + consumed_asset_details = get_consumed_asset_details(args, get_asset_value=False) + for k, v in consumed_asset_details.items(): + if d.meta.has_field(k) and (not d.get(k) or k in force_fields): + d.set(k, v) + + for d in self.service_items: + args = self.as_dict() + args.update(d.as_dict()) + args.doctype = self.doctype + args.name = self.name + service_item_details = get_service_item_details(args) + for k, v in service_item_details.items(): + if d.meta.has_field(k) and (not d.get(k) or k in force_fields): + d.set(k, v) + + def validate_target_item(self): + target_item = frappe.get_cached_doc("Item", self.target_item_code) + + if not target_item.is_fixed_asset and not target_item.is_stock_item: + frappe.throw(_("Target Item {0} is neither a Fixed Asset nor a Stock Item") + .format(target_item.name)) + + if target_item.is_fixed_asset: + self.target_qty = 1 + if flt(self.target_qty) <= 0: + frappe.throw(_("Target Qty must be a positive number")) + + if not target_item.is_stock_item: + self.target_warehouse = None + if not target_item.is_fixed_asset: + self.target_asset = None + self.target_fixed_asset_account = None + if not target_item.has_batch_no: + self.target_batch_no = None + if not target_item.has_serial_no: + self.target_serial_no = "" + + if target_item.is_stock_item and not self.target_warehouse: + frappe.throw(_("Target Warehouse is mandatory for Decapitalization")) + + self.validate_item(target_item) + + def validate_target_asset(self): + if self.target_is_fixed_asset and not self.target_asset: + frappe.throw(_("Target Asset is mandatory for Capitalization")) + + if self.target_asset: + target_asset = self.get_asset_for_validation(self.target_asset) + + if target_asset.item_code != self.target_item_code: + frappe.throw(_("Asset {0} does not belong to Item {1}").format(self.target_asset, self.target_item_code)) + + self.validate_asset(target_asset) + + def validate_consumed_stock_item(self): + for d in self.stock_items: + if d.item_code: + item = frappe.get_cached_doc("Item", d.item_code) + + if not item.is_stock_item: + frappe.throw(_("Row #{0}: Item {1} is not a stock item").format(d.idx, d.item_code)) + + if flt(d.stock_qty) <= 0: + frappe.throw(_("Row #{0}: Qty must be a positive number").format(d.idx)) + + self.validate_item(item) + + def validate_consumed_asset_item(self): + for d in self.asset_items: + if d.asset: + if d.asset == self.target_asset: + frappe.throw(_("Row #{0}: Consumed Asset {1} cannot be the same as the Target Asset") + .format(d.idx, d.asset)) + + asset = self.get_asset_for_validation(d.asset) + self.validate_asset(asset) + + def validate_service_item(self): + for d in self.service_items: + if d.item_code: + item = frappe.get_cached_doc("Item", d.item_code) + + if item.is_stock_item or item.is_fixed_asset: + frappe.throw(_("Row #{0}: Item {1} is not a service item").format(d.idx, d.item_code)) + + if flt(d.qty) <= 0: + frappe.throw(_("Row #{0}: Qty must be a positive number").format(d.idx)) + + if flt(d.rate) <= 0: + frappe.throw(_("Row #{0}: Amount must be a positive number").format(d.idx)) + + self.validate_item(item) + + if not d.cost_center: + d.cost_center = frappe.get_cached_value("Company", self.company, "cost_center") + + def validate_source_mandatory(self): + if not self.target_is_fixed_asset and not self.get('asset_items'): + frappe.throw(_("Consumed Asset Items is mandatory for Decapitalization")) + + if not self.get('stock_items') and not self.get('asset_items'): + frappe.throw(_("Consumed Stock Items or Consumed Asset Items is mandatory for Capitalization")) + + def validate_item(self, item): + from erpnext.stock.doctype.item.item import validate_end_of_life + validate_end_of_life(item.name, item.end_of_life, item.disabled) + + def get_asset_for_validation(self, asset): + return frappe.db.get_value("Asset", asset, ["name", "item_code", "company", "status", "docstatus"], as_dict=1) + + def validate_asset(self, asset): + if asset.status in ("Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"): + frappe.throw(_("Asset {0} is {1}").format(asset.name, asset.status)) + + if asset.docstatus == 0: + frappe.throw(_("Asset {0} is Draft").format(asset.name)) + if asset.docstatus == 2: + frappe.throw(_("Asset {0} is cancelled").format(asset.name)) + + if asset.company != self.company: + frappe.throw(_("Asset {0} does not belong to company {1}").format(asset.name, self.company)) + + @frappe.whitelist() + def set_warehouse_details(self): + for d in self.get('stock_items'): + if d.item_code and d.warehouse: + args = self.get_args_for_incoming_rate(d) + warehouse_details = get_warehouse_details(args) + d.update(warehouse_details) + + @frappe.whitelist() + def set_asset_values(self): + for d in self.get('asset_items'): + if d.asset: + finance_book = d.get('finance_book') or self.get('finance_book') + d.current_asset_value = flt(get_current_asset_value(d.asset, finance_book=finance_book)) + d.asset_value = get_value_after_depreciation_on_disposal_date(d.asset, self.posting_date, + finance_book=finance_book) + + def get_args_for_incoming_rate(self, item): + return frappe._dict({ + "item_code": item.item_code, + "warehouse": item.warehouse, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + "qty": -1 * flt(item.stock_qty), + "serial_no": item.serial_no, + "batch_no": item.batch_no, + "voucher_type": self.doctype, + "voucher_no": self.name, + "company": self.company, + "allow_zero_valuation": cint(item.get('allow_zero_valuation_rate')), + }) + + def calculate_totals(self): + self.stock_items_total = 0 + self.asset_items_total = 0 + self.service_items_total = 0 + + for d in self.stock_items: + d.amount = flt(flt(d.stock_qty) * flt(d.valuation_rate), d.precision('amount')) + self.stock_items_total += d.amount + + for d in self.asset_items: + d.asset_value = flt(flt(d.asset_value), d.precision('asset_value')) + self.asset_items_total += d.asset_value + + for d in self.service_items: + d.amount = flt(flt(d.qty) * flt(d.rate), d.precision('amount')) + self.service_items_total += d.amount + + self.stock_items_total = flt(self.stock_items_total, self.precision('stock_items_total')) + self.asset_items_total = flt(self.asset_items_total, self.precision('asset_items_total')) + self.service_items_total = flt(self.service_items_total, self.precision('service_items_total')) + + self.total_value = self.stock_items_total + self.asset_items_total + self.service_items_total + self.total_value = flt(self.total_value, self.precision('total_value')) + + self.target_qty = flt(self.target_qty, self.precision('target_qty')) + self.target_incoming_rate = self.total_value / self.target_qty + + def update_stock_ledger(self): + sl_entries = [] + + for d in self.stock_items: + sle = self.get_sl_entries(d, { + "actual_qty": -flt(d.stock_qty), + }) + sl_entries.append(sle) + + if not frappe.db.get_value("Item", self.target_item_code, "is_fixed_asset", cache=1): + sle = self.get_sl_entries(self, { + "item_code": self.target_item_code, + "warehouse": self.target_warehouse, + "batch_no": self.target_batch_no, + "serial_no": self.target_serial_no, + "actual_qty": flt(self.target_qty), + "incoming_rate": flt(self.target_incoming_rate) + }) + sl_entries.append(sle) + + # reverse sl entries if cancel + if self.docstatus == 2: + sl_entries.reverse() + + if sl_entries: + self.make_sl_entries(sl_entries) + + def make_gl_entries(self, gl_entries=None, from_repost=False): + from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries + + if not gl_entries: + gl_entries = self.get_gl_entries() + + if self.docstatus == 1: + if gl_entries: + make_gl_entries(gl_entries, from_repost=from_repost) + elif self.docstatus == 2: + make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) + + def get_gl_entries(self, warehouse_account=None, default_expense_account=None, default_cost_center=None): + # Stock GL Entries + gl_entries = [] + + if not warehouse_account: + warehouse_account = get_warehouse_account_map(self.company) + + precision = self.get_debit_field_precision() + sle_map = self.get_stock_ledger_details() + + if self.target_is_fixed_asset: + target_account = self.target_fixed_asset_account + else: + target_account = warehouse_account[self.target_warehouse]["account"] + + target_against = set() + + # Consumed Stock Items + total_consumed_stock_value = 0 + for item_row in self.stock_items: + sle_list = sle_map.get(item_row.name) + if sle_list: + for sle in sle_list: + stock_value_difference = flt(sle.stock_value_difference, precision) + total_consumed_stock_value += -1 * sle.stock_value_difference + + account = warehouse_account[sle.warehouse]["account"] + target_against.add(account) + + gl_entries.append(self.get_gl_dict({ + "account": account, + "against": target_account, + "cost_center": item_row.cost_center, + "project": item_row.get('project') or self.get('project'), + "remarks": self.get("remarks") or "Accounting Entry for Stock", + "credit": -1 * stock_value_difference, + }, warehouse_account[sle.warehouse]["account_currency"], item=item_row)) + + # Consumed Assets + for item in self.asset_items: + asset = self.get_asset(item) + + if self.docstatus == 2: + fixed_asset_gl_entries = get_gl_entries_on_asset_regain(asset, + item.asset_value, item.get('finance_book') or self.get('finance_book')) + asset.db_set("disposal_date", None) + + self.set_consumed_asset_status(asset) + + if asset.calculate_depreciation: + self.reverse_depreciation_entry_made_after_disposal(asset) + self.reset_depreciation_schedule(asset) + else: + if asset.calculate_depreciation: + self.depreciate_asset(asset) + asset.reload() + + fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(asset, + item.asset_value, item.get('finance_book') or self.get('finance_book')) + asset.db_set("disposal_date", self.posting_date) + + self.set_consumed_asset_status(asset) + + for gle in fixed_asset_gl_entries: + gle["against"] = target_account + gl_entries.append(self.get_gl_dict(gle, item=item)) + + # Service Expenses + total_service_expenses = 0 + for item_row in self.service_items: + expense_amount = flt(item_row.amount, precision) + total_service_expenses += expense_amount + target_against.add(item_row.expense_account) + + gl_entries.append(self.get_gl_dict({ + "account": item_row.expense_account, + "against": target_account, + "cost_center": item_row.cost_center, + "project": item_row.get('project') or self.get('project'), + "remarks": self.get("remarks") or "Accounting Entry for Stock", + "credit": expense_amount, + }, item=item_row)) + + target_against = ", ".join(target_against) + total_target_stock_value = 0 + total_target_asset_value = 0 + + if self.target_is_fixed_asset: + # Target Asset Item + total_target_asset_value = flt(self.total_value, precision) + gl_entries.append(self.get_gl_dict({ + "account": self.target_fixed_asset_account, + "against": target_against, + "remarks": self.get("remarks") or _("Accounting Entry for Asset"), + "debit": total_target_asset_value, + "cost_center": self.get('cost_center') + }, item=self)) + + if self.docstatus == 1: + asset_doc = frappe.get_doc("Asset", self.target_asset) + asset_doc.purchase_date = self.posting_date + asset_doc.gross_purchase_amount = total_target_asset_value + asset_doc.purchase_receipt_amount = total_target_asset_value + asset_doc.prepare_depreciation_data() + asset_doc.flags.ignore_validate_update_after_submit = True + asset_doc.save() + else: + # Target Stock Item + sle_list = sle_map.get(self.name) + for sle in sle_list: + stock_value_difference = flt(sle.stock_value_difference, precision) + total_target_stock_value += sle.stock_value_difference + account = warehouse_account[sle.warehouse]["account"] + + gl_entries.append(self.get_gl_dict({ + "account": account, + "against": target_against, + "cost_center": self.cost_center, + "project": self.get('project'), + "remarks": self.get("remarks") or "Accounting Entry for Stock", + "debit": stock_value_difference, + }, warehouse_account[sle.warehouse]["account_currency"], item=self)) + + return gl_entries + + def get_asset(self, item): + asset = frappe.get_doc("Asset", item.asset) + self.check_finance_books(item, asset) + return asset + + def set_consumed_asset_status(self, asset): + if self.docstatus == 1: + asset.set_status("Capitalized" if self.target_is_fixed_asset else "Decapitalized") + else: + asset.set_status() + + +@frappe.whitelist() +def get_target_item_details(item_code=None, company=None): + out = frappe._dict() + + # Get Item Details + item = frappe._dict() + if item_code: + item = frappe.get_cached_doc("Item", item_code) + + # Set Item Details + out.target_item_name = item.item_name + out.target_stock_uom = item.stock_uom + out.target_is_fixed_asset = cint(item.is_fixed_asset) + out.target_has_batch_no = cint(item.has_batch_no) + out.target_has_serial_no = cint(item.has_serial_no) + + if out.target_is_fixed_asset: + out.target_qty = 1 + out.target_warehouse = None + else: + out.target_asset = None + + if not out.target_has_batch_no: + out.target_batch_no = None + if not out.target_has_serial_no: + out.target_serial_no = "" + + # Cost Center + item_defaults = get_item_defaults(item.name, company) + item_group_defaults = get_item_group_defaults(item.name, company) + brand_defaults = get_brand_defaults(item.name, company) + out.cost_center = get_default_cost_center(frappe._dict({'item_code': item.name, 'company': company}), + item_defaults, item_group_defaults, brand_defaults) + + # Set Entry Type + if not item_code: + out.entry_type = "" + elif out.target_is_fixed_asset: + out.entry_type = "Capitalization" + else: + out.entry_type = "Decapitalization" + + return out + + +@frappe.whitelist() +def get_target_asset_details(asset=None, company=None): + out = frappe._dict() + + # Get Asset Details + asset_details = frappe._dict() + if asset: + asset_details = frappe.db.get_value("Asset", asset, ['asset_name', 'item_code'], as_dict=1) + if not asset_details: + frappe.throw(_("Asset {0} does not exist").format(asset)) + + # Re-set item code from Asset + out.target_item_code = asset_details.item_code + + # Set Asset Details + out.asset_name = asset_details.asset_name + + if asset_details.item_code: + out.target_fixed_asset_account = get_asset_category_account('fixed_asset_account', item=asset_details.item_code, + company=company) + else: + out.target_fixed_asset_account = None + + return out + + +@frappe.whitelist() +def get_consumed_stock_item_details(args, get_valuation_rate=True): + if isinstance(args, string_types): + args = json.loads(args) + + args = frappe._dict(args) + out = frappe._dict() + + item = frappe._dict() + if args.item_code: + item = frappe.get_cached_doc("Item", args.item_code) + + out.item_name = item.item_name + out.batch_no = None + out.serial_no = "" + + out.stock_qty = flt(args.stock_qty) or 1 + out.stock_uom = item.stock_uom + + out.warehouse = get_item_warehouse(item, args, overwrite_warehouse=True) if item else None + + # Cost Center + item_defaults = get_item_defaults(item.name, args.company) + item_group_defaults = get_item_group_defaults(item.name, args.company) + brand_defaults = get_brand_defaults(item.name, args.company) + out.cost_center = get_default_cost_center(args, item_defaults, item_group_defaults, brand_defaults) + + if get_valuation_rate: + if args.item_code and out.warehouse: + incoming_rate_args = frappe._dict({ + 'item_code': args.item_code, + 'warehouse': out.warehouse, + 'posting_date': args.posting_date, + 'posting_time': args.posting_time, + 'qty': -1 * flt(out.stock_qty), + "voucher_type": args.doctype, + "voucher_no": args.name, + "company": args.company, + }) + out.update(get_warehouse_details(incoming_rate_args)) + else: + out.valuation_rate = 0 + out.actual_qty = 0 + + return out + + +@frappe.whitelist() +def get_warehouse_details(args): + if isinstance(args, string_types): + args = json.loads(args) + + args = frappe._dict(args) + + out = {} + if args.warehouse and args.item_code: + out = { + "actual_qty": get_previous_sle(args).get("qty_after_transaction") or 0, + "valuation_rate": get_incoming_rate(args, raise_error_if_no_rate=False) + } + return out + + +@frappe.whitelist() +def get_consumed_asset_details(args, get_asset_value=True): + if isinstance(args, string_types): + args = json.loads(args) + + args = frappe._dict(args) + out = frappe._dict() + + asset_details = frappe._dict() + if args.asset: + asset_details = frappe.db.get_value("Asset", args.asset, ['asset_name', 'item_code', 'item_name'], as_dict=1) + if not asset_details: + frappe.throw(_("Asset {0} does not exist").format(args.asset)) + + out.item_code = asset_details.item_code + out.asset_name = asset_details.asset_name + out.item_name = asset_details.item_name + + if get_asset_value: + if args.asset: + out.current_asset_value = flt(get_current_asset_value(args.asset, finance_book=args.finance_book)) + out.asset_value = get_value_after_depreciation_on_disposal_date(args.asset, args.posting_date, + finance_book=args.finance_book) + else: + out.current_asset_value = 0 + out.asset_value = 0 + + # Account + if asset_details.item_code: + out.fixed_asset_account = get_asset_category_account('fixed_asset_account', item=asset_details.item_code, + company=args.company) + else: + out.fixed_asset_account = None + + # Cost Center + if asset_details.item_code: + item = frappe.get_cached_doc("Item", asset_details.item_code) + item_defaults = get_item_defaults(item.name, args.company) + item_group_defaults = get_item_group_defaults(item.name, args.company) + brand_defaults = get_brand_defaults(item.name, args.company) + out.cost_center = get_default_cost_center(args, item_defaults, item_group_defaults, brand_defaults) + + return out + + +@frappe.whitelist() +def get_service_item_details(args): + if isinstance(args, string_types): + args = json.loads(args) + + args = frappe._dict(args) + out = frappe._dict() + + item = frappe._dict() + if args.item_code: + item = frappe.get_cached_doc("Item", args.item_code) + + out.item_name = item.item_name + out.qty = flt(args.qty) or 1 + out.uom = item.purchase_uom or item.stock_uom + + item_defaults = get_item_defaults(item.name, args.company) + item_group_defaults = get_item_group_defaults(item.name, args.company) + brand_defaults = get_brand_defaults(item.name, args.company) + + out.expense_account = get_default_expense_account(args, item_defaults, item_group_defaults, brand_defaults) + out.cost_center = get_default_cost_center(args, item_defaults, item_group_defaults, brand_defaults) + + return out diff --git a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py new file mode 100644 index 00000000000..7046de6f837 --- /dev/null +++ b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py @@ -0,0 +1,335 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import unittest + +import frappe +from frappe.utils import cint, flt, getdate, now_datetime + +from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries +from erpnext.assets.doctype.asset.test_asset import ( + create_asset, + create_asset_data, + set_depreciation_settings_in_company, +) +from erpnext.stock.doctype.item.test_item import create_item + + +class TestAssetCapitalization(unittest.TestCase): + def setUp(self): + set_depreciation_settings_in_company() + create_asset_data() + create_asset_capitalization_data() + frappe.db.sql("delete from `tabTax Rule`") + + def test_capitalization(self): + # Variables + consumed_asset_value = 100_000 + + stock_rate = 1000 + stock_qty = 2 + stock_amount = 2000 + + service_rate = 500 + service_qty = 2 + service_amount = 1000 + + total_amount = 103_000 + + # Create assets + target_asset = create_asset(asset_name='Asset Capitalization Target Asset', submit=1) + consumed_asset = create_asset(asset_name='Asset Capitalization Consumable Asset', asset_value=consumed_asset_value, + submit=1) + + # Create and submit Asset Captitalization + asset_capitalization = create_asset_capitalization(target_asset=target_asset.name, + stock_qty=stock_qty, stock_rate=stock_rate, + consumed_asset=consumed_asset.name, + service_qty=service_qty, service_rate=service_rate, + service_expense_account='Expenses Included In Asset Valuation - _TC', + submit=1) + + # Test Asset Capitalization values + self.assertEqual(asset_capitalization.entry_type, 'Capitalization') + self.assertEqual(asset_capitalization.target_qty, 1) + + self.assertEqual(asset_capitalization.stock_items[0].valuation_rate, stock_rate) + self.assertEqual(asset_capitalization.stock_items[0].amount, stock_amount) + self.assertEqual(asset_capitalization.stock_items_total, stock_amount) + + self.assertEqual(asset_capitalization.asset_items[0].asset_value, consumed_asset_value) + self.assertEqual(asset_capitalization.asset_items_total, consumed_asset_value) + + self.assertEqual(asset_capitalization.service_items[0].amount, service_amount) + self.assertEqual(asset_capitalization.service_items_total, service_amount) + + self.assertEqual(asset_capitalization.total_value, total_amount) + self.assertEqual(asset_capitalization.target_incoming_rate, total_amount) + + # Test Target Asset values + target_asset.reload() + self.assertEqual(target_asset.gross_purchase_amount, total_amount) + self.assertEqual(target_asset.purchase_receipt_amount, total_amount) + + # Test Consumed Asset values + self.assertEqual(consumed_asset.db_get('status'), 'Capitalized') + + # Test General Ledger Entries + expected_gle = { + '_Test Fixed Asset - _TC': 3000, + 'Expenses Included In Asset Valuation - _TC': -1000, + 'Stock In Hand - _TC' : -2000 + } + actual_gle = get_actual_gle_dict(asset_capitalization.name) + + self.assertEqual(actual_gle, expected_gle) + + # Test Stock Ledger Entries + expected_sle = { + ('Capitalization Source Stock Item', '_Test Warehouse - _TC'): { + 'actual_qty': -stock_qty, 'stock_value_difference': -stock_amount + } + } + actual_sle = get_actual_sle_dict(asset_capitalization.name) + + self.assertEqual(actual_sle, expected_sle) + + # Cancel Asset Capitalization and make test entries and status are reversed + asset_capitalization.cancel() + self.assertEqual(consumed_asset.db_get('status'), 'Submitted') + self.assertFalse(get_actual_gle_dict(asset_capitalization.name)) + self.assertFalse(get_actual_sle_dict(asset_capitalization.name)) + + def test_decapitalization_with_depreciation(self): + # Variables + purchase_date = '2020-01-01' + depreciation_start_date = '2020-12-31' + capitalization_date = '2021-06-30' + + total_number_of_depreciations = 3 + expected_value_after_useful_life = 10_000 + consumed_asset_purchase_value = 100_000 + consumed_asset_current_value = 70_000 + consumed_asset_value_before_disposal = 55_000 + + target_qty = 10 + target_incoming_rate = 5500 + + depreciation_before_disposal_amount = 15_000 + accumulated_depreciation = 45_000 + + # to accomodate for depreciation on disposal calculation minor difference + consumed_asset_value_before_disposal = 55_123.29 + target_incoming_rate = 5512.329 + depreciation_before_disposal_amount = 14_876.71 + accumulated_depreciation = 44_876.71 + + # Create assets + consumed_asset = create_depreciation_asset( + asset_name='Asset Capitalization Consumable Asset', + asset_value=consumed_asset_purchase_value, + purchase_date=purchase_date, + depreciation_start_date=depreciation_start_date, + depreciation_method='Straight Line', + total_number_of_depreciations=total_number_of_depreciations, + frequency_of_depreciation=12, + expected_value_after_useful_life=expected_value_after_useful_life, + submit=1) + + # Create and submit Asset Captitalization + asset_capitalization = create_asset_capitalization( + posting_date=capitalization_date, # half a year + target_item_code="Capitalization Target Stock Item", + target_qty=target_qty, + consumed_asset=consumed_asset.name, + submit=1) + + # Test Asset Capitalization values + self.assertEqual(asset_capitalization.entry_type, 'Decapitalization') + + self.assertEqual(asset_capitalization.asset_items[0].current_asset_value, consumed_asset_current_value) + self.assertEqual(asset_capitalization.asset_items[0].asset_value, consumed_asset_value_before_disposal) + self.assertEqual(asset_capitalization.asset_items_total, consumed_asset_value_before_disposal) + + self.assertEqual(asset_capitalization.total_value, consumed_asset_value_before_disposal) + self.assertEqual(asset_capitalization.target_incoming_rate, target_incoming_rate) + + # Test Consumed Asset values + consumed_asset.reload() + self.assertEqual(consumed_asset.status, 'Decapitalized') + + consumed_depreciation_schedule = [d for d in consumed_asset.schedules + if getdate(d.schedule_date) == getdate(capitalization_date)] + self.assertTrue(consumed_depreciation_schedule and consumed_depreciation_schedule[0].journal_entry) + self.assertEqual(consumed_depreciation_schedule[0].depreciation_amount, depreciation_before_disposal_amount) + + # Test General Ledger Entries + expected_gle = { + 'Stock In Hand - _TC': consumed_asset_value_before_disposal, + '_Test Accumulated Depreciations - _TC': accumulated_depreciation, + '_Test Fixed Asset - _TC': -consumed_asset_purchase_value, + } + actual_gle = get_actual_gle_dict(asset_capitalization.name) + + self.assertEqual(actual_gle, expected_gle) + + # Cancel Asset Capitalization and make test entries and status are reversed + asset_capitalization.reload() + asset_capitalization.cancel() + self.assertEqual(consumed_asset.db_get('status'), 'Partially Depreciated') + self.assertFalse(get_actual_gle_dict(asset_capitalization.name)) + self.assertFalse(get_actual_sle_dict(asset_capitalization.name)) + + +def create_asset_capitalization_data(): + create_item("Capitalization Target Stock Item", + is_stock_item=1, is_fixed_asset=0, is_purchase_item=0) + create_item("Capitalization Source Stock Item", + is_stock_item=1, is_fixed_asset=0, is_purchase_item=0) + create_item("Capitalization Source Service Item", + is_stock_item=0, is_fixed_asset=0, is_purchase_item=0) + + +def create_asset_capitalization(**args): + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + args = frappe._dict(args) + + now = now_datetime() + target_asset = frappe.get_doc("Asset", args.target_asset) if args.target_asset else frappe._dict() + target_item_code = target_asset.item_code or args.target_item_code + company = target_asset.company or args.company or "_Test Company" + warehouse = args.warehouse or create_warehouse("_Test Warehouse", company=company) + target_warehouse = args.target_warehouse or warehouse + source_warehouse = args.source_warehouse or warehouse + + asset_capitalization = frappe.new_doc("Asset Capitalization") + asset_capitalization.update({ + "company": company, + "posting_date": args.posting_date or now.strftime('%Y-%m-%d'), + "posting_time": args.posting_time or now.strftime('%H:%M:%S.%f'), + "target_item_code": target_item_code, + "target_asset": target_asset.name, + "target_warehouse": target_warehouse, + "target_qty": flt(args.target_qty) or 1, + "target_batch_no": args.target_batch_no, + "target_serial_no": args.target_serial_no, + "finance_book": args.finance_book + }) + + if args.posting_date or args.posting_time: + asset_capitalization.set_posting_time = 1 + + if flt(args.stock_rate): + asset_capitalization.append("stock_items", { + "item_code": args.stock_item or "Capitalization Source Stock Item", + "warehouse": source_warehouse, + "stock_qty": flt(args.stock_qty) or 1, + "batch_no": args.stock_batch_no, + "serial_no": args.stock_serial_no, + }) + + if args.consumed_asset: + asset_capitalization.append("asset_items", { + "asset": args.consumed_asset, + }) + + if flt(args.service_rate): + asset_capitalization.append("service_items", { + "item_code": args.service_item or "Capitalization Source Service Item", + "expense_account": args.service_expense_account, + "qty": flt(args.service_qty) or 1, + "rate": flt(args.service_rate) + }) + + if args.submit: + create_stock_reconciliation(asset_capitalization, stock_rate=args.stock_rate) + + asset_capitalization.insert() + + if args.submit: + asset_capitalization.submit() + + return asset_capitalization + + +def create_stock_reconciliation(asset_capitalization, stock_rate=0): + from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( + create_stock_reconciliation, + ) + if not asset_capitalization.get('stock_items'): + return + + return create_stock_reconciliation( + item_code=asset_capitalization.stock_items[0].item_code, + warehouse=asset_capitalization.stock_items[0].warehouse, + qty=flt(asset_capitalization.stock_items[0].stock_qty), + rate=flt(stock_rate), + company=asset_capitalization.company) + + +def create_depreciation_asset(**args): + args = frappe._dict(args) + + asset = frappe.new_doc("Asset") + asset.is_existing_asset = 1 + asset.calculate_depreciation = 1 + asset.asset_owner = "Company" + + asset.company = args.company or "_Test Company" + asset.item_code = args.item_code or "Macbook Pro" + asset.asset_name = args.asset_name or asset.item_code + asset.location = args.location or "Test Location" + + asset.purchase_date = args.purchase_date or '2020-01-01' + asset.available_for_use_date = args.available_for_use_date or asset.purchase_date + + asset.gross_purchase_amount = args.asset_value or 100000 + asset.purchase_receipt_amount = asset.gross_purchase_amount + + finance_book = asset.append('finance_books') + finance_book.depreciation_start_date = args.depreciation_start_date or '2020-12-31' + finance_book.depreciation_method = args.depreciation_method or 'Straight Line' + finance_book.total_number_of_depreciations = cint(args.total_number_of_depreciations) or 3 + finance_book.frequency_of_depreciation = cint(args.frequency_of_depreciation) or 12 + finance_book.expected_value_after_useful_life = flt(args.expected_value_after_useful_life) + + if args.submit: + asset.submit() + + frappe.db.set_value("Company", "_Test Company", "series_for_depreciation_entry", "DEPR-") + post_depreciation_entries(date=finance_book.depreciation_start_date) + asset.load_from_db() + + return asset + + +def get_actual_gle_dict(name): + return dict(frappe.db.sql(""" + select account, sum(debit-credit) as diff + from `tabGL Entry` + where voucher_type = 'Asset Capitalization' and voucher_no = %s + group by account + having diff != 0 + """, name)) + + +def get_actual_sle_dict(name): + sles = frappe.db.sql(""" + select + item_code, warehouse, + sum(actual_qty) as actual_qty, + sum(stock_value_difference) as stock_value_difference + from `tabStock Ledger Entry` + where voucher_type = 'Asset Capitalization' and voucher_no = %s + group by item_code, warehouse + having actual_qty != 0 + """, name, as_dict=1) + + sle_dict = {} + for d in sles: + sle_dict[(d.item_code, d.warehouse)] = { + 'actual_qty': d.actual_qty, 'stock_value_difference': d.stock_value_difference + } + + return sle_dict diff --git a/erpnext/assets/doctype/asset_capitalization_asset_item/__init__.py b/erpnext/assets/doctype/asset_capitalization_asset_item/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/assets/doctype/asset_capitalization_asset_item/asset_capitalization_asset_item.json b/erpnext/assets/doctype/asset_capitalization_asset_item/asset_capitalization_asset_item.json new file mode 100644 index 00000000000..ebaaffbad15 --- /dev/null +++ b/erpnext/assets/doctype/asset_capitalization_asset_item/asset_capitalization_asset_item.json @@ -0,0 +1,128 @@ +{ + "actions": [], + "creation": "2021-09-05 15:52:10.124538", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "asset", + "asset_name", + "finance_book", + "column_break_3", + "item_code", + "item_name", + "section_break_6", + "current_asset_value", + "asset_value", + "column_break_9", + "accounting_dimensions_section", + "cost_center", + "dimension_col_break", + "fixed_asset_account" + ], + "fields": [ + { + "fieldname": "asset", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Asset", + "options": "Asset", + "reqd": 1 + }, + { + "fetch_from": "asset.asset_name", + "fieldname": "asset_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Asset Name", + "read_only": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fetch_from": "asset.item_code", + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "options": "Item", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "item_name", + "fieldtype": "Data", + "label": "Item Name", + "read_only": 1 + }, + { + "fieldname": "section_break_6", + "fieldtype": "Section Break", + "label": "Value" + }, + { + "default": "0", + "fieldname": "asset_value", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Asset Value", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "fieldname": "fixed_asset_account", + "fieldtype": "Link", + "label": "Fixed Asset Account", + "options": "Account", + "read_only": 1 + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "collapsible": 1, + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" + }, + { + "fieldname": "finance_book", + "fieldtype": "Link", + "label": "Finance Book", + "options": "Finance Book" + }, + { + "fieldname": "current_asset_value", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Current Asset Value", + "options": "Company:company:default_currency", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-09-12 14:30:02.915132", + "modified_by": "Administrator", + "module": "Assets", + "name": "Asset Capitalization Asset Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/assets/doctype/asset_capitalization_asset_item/asset_capitalization_asset_item.py b/erpnext/assets/doctype/asset_capitalization_asset_item/asset_capitalization_asset_item.py new file mode 100644 index 00000000000..ba356d6b9f0 --- /dev/null +++ b/erpnext/assets/doctype/asset_capitalization_asset_item/asset_capitalization_asset_item.py @@ -0,0 +1,9 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class AssetCapitalizationAssetItem(Document): + pass diff --git a/erpnext/assets/doctype/asset_capitalization_service_item/__init__.py b/erpnext/assets/doctype/asset_capitalization_service_item/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/assets/doctype/asset_capitalization_service_item/asset_capitalization_service_item.json b/erpnext/assets/doctype/asset_capitalization_service_item/asset_capitalization_service_item.json new file mode 100644 index 00000000000..0ae1c1428ee --- /dev/null +++ b/erpnext/assets/doctype/asset_capitalization_service_item/asset_capitalization_service_item.json @@ -0,0 +1,122 @@ +{ + "actions": [], + "creation": "2021-09-06 13:32:08.642060", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item_code", + "item_name", + "column_break_3", + "expense_account", + "section_break_6", + "qty", + "uom", + "column_break_9", + "rate", + "amount", + "accounting_dimensions_section", + "cost_center", + "dimension_col_break" + ], + "fields": [ + { + "bold": 1, + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "options": "Item" + }, + { + "fetch_from": "item_code.item_name", + "fieldname": "item_name", + "fieldtype": "Data", + "label": "Item Name", + "read_only": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "expense_account", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Expense Account", + "options": "Account", + "reqd": 1 + }, + { + "fieldname": "section_break_6", + "fieldtype": "Section Break", + "label": "Qty and Rate" + }, + { + "columns": 1, + "default": "1", + "fieldname": "qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Qty", + "non_negative": 1 + }, + { + "columns": 1, + "fetch_from": "stock_item_code.stock_uom", + "fieldname": "uom", + "fieldtype": "Link", + "in_list_view": 1, + "label": "UOM", + "options": "UOM" + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "fieldname": "rate", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Rate", + "options": "Company:company:default_currency" + }, + { + "default": "0", + "fieldname": "amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Amount", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-09-08 15:52:08.598100", + "modified_by": "Administrator", + "module": "Assets", + "name": "Asset Capitalization Service Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/assets/doctype/asset_capitalization_service_item/asset_capitalization_service_item.py b/erpnext/assets/doctype/asset_capitalization_service_item/asset_capitalization_service_item.py new file mode 100644 index 00000000000..28d018ee39a --- /dev/null +++ b/erpnext/assets/doctype/asset_capitalization_service_item/asset_capitalization_service_item.py @@ -0,0 +1,9 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class AssetCapitalizationServiceItem(Document): + pass diff --git a/erpnext/assets/doctype/asset_capitalization_stock_item/__init__.py b/erpnext/assets/doctype/asset_capitalization_stock_item/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json new file mode 100644 index 00000000000..14eb0f6ef20 --- /dev/null +++ b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.json @@ -0,0 +1,156 @@ +{ + "actions": [], + "creation": "2021-09-05 15:23:23.492310", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item_code", + "item_name", + "column_break_3", + "warehouse", + "section_break_6", + "stock_qty", + "stock_uom", + "actual_qty", + "column_break_9", + "valuation_rate", + "amount", + "batch_and_serial_no_section", + "batch_no", + "column_break_13", + "serial_no", + "accounting_dimensions_section", + "cost_center", + "dimension_col_break" + ], + "fields": [ + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "warehouse", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Warehouse", + "options": "Warehouse", + "reqd": 1 + }, + { + "fieldname": "batch_no", + "fieldtype": "Link", + "label": "Batch No", + "options": "Batch" + }, + { + "fieldname": "section_break_6", + "fieldtype": "Section Break", + "label": "Qty and Rate" + }, + { + "columns": 1, + "fieldname": "stock_qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Qty", + "non_negative": 1 + }, + { + "columns": 1, + "fetch_from": "stock_item_code.stock_uom", + "fieldname": "stock_uom", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Stock UOM", + "options": "UOM", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "fieldname": "valuation_rate", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Valuation Rate", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Amount", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "batch_and_serial_no_section", + "fieldtype": "Section Break", + "label": "Batch and Serial No" + }, + { + "fieldname": "column_break_13", + "fieldtype": "Column Break" + }, + { + "fieldname": "serial_no", + "fieldtype": "Small Text", + "label": "Serial No" + }, + { + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "options": "Item", + "reqd": 1 + }, + { + "fetch_from": "item_code.item_name", + "fieldname": "item_name", + "fieldtype": "Data", + "label": "Item Name", + "read_only": 1 + }, + { + "fieldname": "actual_qty", + "fieldtype": "Float", + "label": "Actual Qty in Warehouse", + "no_copy": 1, + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-09-08 15:56:20.230548", + "modified_by": "Administrator", + "module": "Assets", + "name": "Asset Capitalization Stock Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.py b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.py new file mode 100644 index 00000000000..5d6f98d5cf0 --- /dev/null +++ b/erpnext/assets/doctype/asset_capitalization_stock_item/asset_capitalization_stock_item.py @@ -0,0 +1,9 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class AssetCapitalizationStockItem(Document): + pass diff --git a/erpnext/assets/workspace/assets/assets.json b/erpnext/assets/workspace/assets/assets.json index 26a6609b310..c07155e48a8 100644 --- a/erpnext/assets/workspace/assets/assets.json +++ b/erpnext/assets/workspace/assets/assets.json @@ -130,6 +130,17 @@ "onboard": 0, "type": "Link" }, + { + "dependencies": "Asset", + "hidden": 0, + "is_query_report": 0, + "label": "Asset Capitalization", + "link_count": 0, + "link_to": "Asset Capitalization", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, { "hidden": 0, "is_query_report": 0, @@ -172,7 +183,7 @@ "type": "Link" } ], - "modified": "2022-01-13 17:25:41.730628", + "modified": "2022-01-13 18:25:41.730628", "modified_by": "Administrator", "module": "Assets", "name": "Assets", @@ -205,4 +216,4 @@ } ], "title": "Assets" -} \ No newline at end of file +} diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 6f321f47661..a097dd7b6bf 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -38,6 +38,7 @@ from erpnext.accounts.party import ( validate_party_frozen_disabled, ) from erpnext.accounts.utils import get_account_currency, get_fiscal_years, validate_fiscal_year +from erpnext.assets.doctype.asset.depreciation import make_depreciation_entry from erpnext.buying.utils import update_last_purchase_rate from erpnext.controllers.print_settings import ( set_print_templates_for_item_table, @@ -1870,6 +1871,99 @@ class AccountsController(TransactionBase): ): throw(_("Conversion rate cannot be 0 or 1")) + def check_finance_books(self, item, asset): + if ( + len(asset.finance_books) > 1 + and not item.get("finance_book") + and not self.get("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(date_of_disposal=self.posting_date) + asset.save() + + make_depreciation_entry(asset.name, 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(date_of_return=self.posting_date) + + self.modify_depreciation_schedule_for_asset_repairs(asset) + asset.save() + + 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 reverse_depreciation_entry_made_after_disposal(self, asset): + from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry + + posting_date_of_original_disposal = self.get_posting_date_of_disposal_entry() + + 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_disposal: + if not self.disposal_was_made_on_original_schedule_date( + asset, schedule, row, posting_date_of_original_disposal + ) or self.disposal_happens_in_the_future(posting_date_of_original_disposal): + + reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry) + reverse_journal_entry.posting_date = nowdate() + frappe.flags.is_reverse_depr_entry = True + reverse_journal_entry.submit() + + frappe.flags.is_reverse_depr_entry = False + asset.flags.ignore_validate_update_after_submit = True + schedule.journal_entry = None + asset.save() + + def get_posting_date_of_disposal_entry(self): + if self.doctype == "Sales Invoice" and self.return_against: + return frappe.db.get_value("Sales Invoice", self.return_against, "posting_date") + else: + return self.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 disposal_was_made_on_original_schedule_date( + self, asset, schedule, row, posting_date_of_disposal + ): + 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_disposal: + return True + return False + + def disposal_happens_in_the_future(self, posting_date_of_disposal): + if posting_date_of_disposal > getdate(): + return True + + return False + @frappe.whitelist() def get_tax_rate(account_head):