# Copyright (c) 2014 ACSONE SA/NV (acsone.eu). # Copyright 2009-2018 Noviat # Copyright 2021 Tecnativa - João Marques # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import calendar import time from datetime import date, datetime from odoo import fields from odoo.tests.common import Form from odoo.addons.account.tests.common import AccountTestInvoicingCommon class TestAssetManagement(AccountTestInvoicingCommon): @classmethod def setUpClass(cls): super().setUpClass() # ENVIRONMENTS cls.asset_model = cls.env["account.asset"] cls.asset_profile_model = cls.env["account.asset.profile"] cls.dl_model = cls.env["account.asset.line"] cls.remove_model = cls.env["account.asset.remove"] # INSTANCES cls.partner = cls.env["res.partner"].create({"name": "Test Partner"}) cls.product = cls.env["product.product"].create( {"name": "Test", "standard_price": 500.0} ) move_form = Form( cls.env["account.move"].with_context( default_move_type="in_invoice", check_move_validity=False ) ) move_form.invoice_date = fields.Date.context_today(cls.env.user) move_form.partner_id = cls.partner with move_form.invoice_line_ids.new() as line_form: line_form.name = "test" line_form.product_id = cls.product line_form.price_unit = 2000.00 line_form.quantity = 1 cls.invoice = move_form.save() move_form = Form( cls.env["account.move"].with_context( default_move_type="in_invoice", check_move_validity=False ) ) move_form.invoice_date = fields.Date.context_today(cls.env.user) move_form.partner_id = cls.partner with move_form.invoice_line_ids.new() as line_form: line_form.name = "test 2" line_form.product_id = cls.product line_form.price_unit = 10000.00 line_form.quantity = 1 with move_form.invoice_line_ids.new() as line_form: line_form.name = "test 3" line_form.product_id = cls.product line_form.price_unit = 20000.00 line_form.quantity = 1 cls.invoice_2 = move_form.save() # analytic configuration cls.env.user.write( { "groups_id": [ (4, cls.env.ref("analytic.group_analytic_accounting").id), (4, cls.env.ref("analytic.group_analytic_tags").id), ], } ) cls.analytic_account = cls.env["account.analytic.account"].create( {"name": "test_analytic_account"} ) cls.analytic_tag = cls.env["account.analytic.tag"].create( {"name": "test_analytic_tag"} ) # Asset Profile 1 cls.ict3Y = cls.asset_profile_model.create( { "account_expense_depreciation_id": cls.company_data[ "default_account_expense" ].id, "account_asset_id": cls.company_data["default_account_assets"].id, "account_depreciation_id": cls.company_data[ "default_account_assets" ].id, "journal_id": cls.company_data["default_journal_purchase"].id, "name": "Hardware - 3 Years", "method_time": "year", "method_number": 3, "method_period": "year", } ) # Asset Profile 2 cls.car5y = cls.asset_profile_model.create( { "account_expense_depreciation_id": cls.company_data[ "default_account_expense" ].id, "account_asset_id": cls.company_data["default_account_assets"].id, "account_depreciation_id": cls.company_data[ "default_account_assets" ].id, "journal_id": cls.company_data["default_journal_purchase"].id, "name": "Cars - 5 Years", "method_time": "year", "method_number": 5, "method_period": "year", "account_analytic_id": cls.analytic_account.id, "analytic_tag_ids": [(4, cls.analytic_tag.id)], } ) def test_invoice_line_without_product(self): tax = self.env["account.tax"].create( { "name": "TAX 15%", "amount_type": "percent", "type_tax_use": "purchase", "amount": 15.0, } ) move_form = Form( self.env["account.move"].with_context( default_move_type="in_invoice", check_move_validity=False ) ) move_form.invoice_date = fields.Date.context_today(self.env.user) move_form.partner_id = self.partner with move_form.invoice_line_ids.new() as line_form: line_form.name = "Line 1" line_form.price_unit = 200.0 line_form.quantity = 1 line_form.tax_ids.clear() line_form.tax_ids.add(tax) invoice = move_form.save() self.assertEqual(invoice.partner_id, self.partner) def test_00_fiscalyear_lock_date_month(self): asset = self.asset_model.create( { "name": "test asset", "profile_id": self.car5y.id, "purchase_value": 1500, "date_start": "1901-02-01", "method_time": "year", "method_number": 3, "method_period": "month", } ) asset.compute_depreciation_board() asset.refresh() self.assertTrue(asset.depreciation_line_ids[0].init_entry) for i in range(1, 36): self.assertFalse(asset.depreciation_line_ids[i].init_entry) def test_00_fiscalyear_lock_date_year(self): asset = self.asset_model.create( { "name": "test asset", "profile_id": self.car5y.id, "purchase_value": 1500, "date_start": "1901-02-01", "method_time": "year", "method_number": 3, "method_period": "year", } ) asset.compute_depreciation_board() asset.refresh() self.assertTrue(asset.depreciation_line_ids[0].init_entry) for i in range(1, 4): self.assertFalse(asset.depreciation_line_ids[i].init_entry) def test_01_nonprorata_basic(self): """Basic tests of depreciation board computations and postings.""" # First create demo assets and do some sanity checks # Asset Model 1 ict0 = self.asset_model.create( { "state": "draft", "method_time": "year", "method_number": 3, "method_period": "year", "name": "Laptop", "code": "PI00101", "purchase_value": 1500.0, "profile_id": self.ict3Y.id, "date_start": time.strftime("%Y-01-01"), } ) # Sanity checks self.assertEqual(ict0.state, "draft") self.assertEqual(ict0.purchase_value, 1500) self.assertEqual(ict0.salvage_value, 0) self.assertEqual(ict0.depreciation_base, 1500) self.assertEqual(len(ict0.depreciation_line_ids), 1) # Asset Model 2 vehicle0 = self.asset_model.create( { "state": "draft", "method_time": "year", "method_number": 5, "method_period": "year", "name": "CEO's Car", "purchase_value": 12000.0, "salvage_value": 2000.0, "profile_id": self.car5y.id, "date_start": time.strftime("%Y-01-01"), } ) # Sanity checks self.assertEqual(vehicle0.state, "draft") self.assertEqual(vehicle0.purchase_value, 12000) self.assertEqual(vehicle0.salvage_value, 2000) self.assertEqual(vehicle0.depreciation_base, 10000) self.assertEqual(len(vehicle0.depreciation_line_ids), 1) # Compute the depreciation boards ict0.compute_depreciation_board() ict0.refresh() self.assertEqual(len(ict0.depreciation_line_ids), 4) self.assertEqual(ict0.depreciation_line_ids[1].amount, 500) vehicle0.compute_depreciation_board() vehicle0.refresh() self.assertEqual(len(vehicle0.depreciation_line_ids), 6) self.assertEqual(vehicle0.depreciation_line_ids[1].amount, 2000) # Post the first depreciation line ict0.validate() ict0.depreciation_line_ids[1].create_move() ict0.refresh() self.assertEqual(ict0.state, "open") self.assertEqual(ict0.value_depreciated, 500) self.assertEqual(ict0.value_residual, 1000) vehicle0.validate() created_move_ids = vehicle0.depreciation_line_ids[1].create_move() for move_id in created_move_ids: move = self.env["account.move"].browse(move_id) expense_line = move.line_ids.filtered( lambda line: line.account_id.internal_group == "expense" ) self.assertEqual( expense_line.analytic_account_id.id, self.analytic_account.id, ) self.assertEqual(expense_line.analytic_tag_ids.id, self.analytic_tag.id) vehicle0.refresh() self.assertEqual(vehicle0.state, "open") self.assertEqual(vehicle0.value_depreciated, 2000) self.assertEqual(vehicle0.value_residual, 8000) def test_02_prorata_basic(self): """Prorata temporis depreciation basic test.""" asset = self.asset_model.create( { "name": "test asset", "profile_id": self.car5y.id, "purchase_value": 3333, "salvage_value": 0, "date_start": time.strftime("%Y-07-07"), "method_time": "year", "method_number": 5, "method_period": "month", "prorata": True, } ) asset.compute_depreciation_board() asset.refresh() if calendar.isleap(date.today().year): self.assertAlmostEqual( asset.depreciation_line_ids[1].amount, 46.44, places=2 ) else: self.assertAlmostEqual( asset.depreciation_line_ids[1].amount, 47.33, places=2 ) self.assertAlmostEqual(asset.depreciation_line_ids[2].amount, 55.55, places=2) self.assertAlmostEqual(asset.depreciation_line_ids[3].amount, 55.55, places=2) self.assertAlmostEqual(asset.depreciation_line_ids[4].amount, 55.55, places=2) self.assertAlmostEqual(asset.depreciation_line_ids[5].amount, 55.55, places=2) self.assertAlmostEqual(asset.depreciation_line_ids[6].amount, 55.55, places=2) if calendar.isleap(date.today().year): self.assertAlmostEqual( asset.depreciation_line_ids[-1].amount, 9.11, places=2 ) else: self.assertAlmostEqual( asset.depreciation_line_ids[-1].amount, 8.22, places=2 ) def test_03_proprata_init_prev_year(self): """Prorata temporis depreciation with init value in prev year.""" # Create an asset in current year asset = self.asset_model.create( { "name": "test asset", "profile_id": self.car5y.id, "purchase_value": 3333, "salvage_value": 0, "date_start": "%d-07-07" % (datetime.now().year - 1,), "method_time": "year", "method_number": 5, "method_period": "month", "prorata": True, } ) # Create a initial depreciation line in previous year self.dl_model.create( { "asset_id": asset.id, "amount": 325.08, "line_date": "%d-12-31" % (datetime.now().year - 1,), "type": "depreciate", "init_entry": True, } ) self.assertEqual(len(asset.depreciation_line_ids), 2) asset.compute_depreciation_board() asset.refresh() # check the depreciated value is the initial value self.assertAlmostEqual(asset.value_depreciated, 325.08, places=2) # check computed values in the depreciation board self.assertAlmostEqual(asset.depreciation_line_ids[3].amount, 55.55, places=2) if calendar.isleap(date.today().year - 1): # for leap years the first year depreciation amount of 325.08 # is too high and hence a correction is applied to the next # entry of the table self.assertAlmostEqual( asset.depreciation_line_ids[2].amount, 54.66, places=2 ) self.assertAlmostEqual( asset.depreciation_line_ids[3].amount, 55.55, places=2 ) self.assertAlmostEqual( asset.depreciation_line_ids[-1].amount, 9.11, places=2 ) else: self.assertAlmostEqual( asset.depreciation_line_ids[2].amount, 55.55, places=2 ) self.assertAlmostEqual( asset.depreciation_line_ids[-1].amount, 8.22, places=2 ) def test_04_prorata_init_cur_year(self): """Prorata temporis depreciation with init value in curent year.""" asset = self.asset_model.create( { "name": "test asset", "profile_id": self.car5y.id, "purchase_value": 3333, "salvage_value": 0, "date_start": time.strftime("%Y-07-07"), "method_time": "year", "method_number": 5, "method_period": "month", "prorata": True, } ) self.dl_model.create( { "asset_id": asset.id, "amount": 279.44, "line_date": time.strftime("%Y-11-30"), "type": "depreciate", "init_entry": True, } ) self.assertEqual(len(asset.depreciation_line_ids), 2) asset.compute_depreciation_board() asset.refresh() # check the depreciated value is the initial value self.assertAlmostEqual(asset.value_depreciated, 279.44, places=2) # check computed values in the depreciation board if calendar.isleap(date.today().year): self.assertAlmostEqual( asset.depreciation_line_ids[2].amount, 44.75, places=2 ) else: self.assertAlmostEqual( asset.depreciation_line_ids[2].amount, 45.64, places=2 ) self.assertAlmostEqual(asset.depreciation_line_ids[3].amount, 55.55, places=2) if calendar.isleap(date.today().year): self.assertAlmostEqual( asset.depreciation_line_ids[-1].amount, 9.11, places=2 ) else: self.assertAlmostEqual( asset.depreciation_line_ids[-1].amount, 8.22, places=2 ) def test_05_degressive_linear(self): """Degressive-Linear with annual and quarterly depreciation.""" # annual depreciation asset = self.asset_model.create( { "name": "test asset", "profile_id": self.car5y.id, "purchase_value": 1000, "salvage_value": 0, "date_start": time.strftime("%Y-07-07"), "method_time": "year", "method": "degr-linear", "method_progress_factor": 0.40, "method_number": 5, "method_period": "year", "prorata": False, } ) asset.compute_depreciation_board() asset.refresh() # check values in the depreciation board self.assertEqual(len(asset.depreciation_line_ids), 5) self.assertAlmostEqual(asset.depreciation_line_ids[1].amount, 400.00, places=2) self.assertAlmostEqual(asset.depreciation_line_ids[2].amount, 240.00, places=2) self.assertAlmostEqual(asset.depreciation_line_ids[3].amount, 200.00, places=2) self.assertAlmostEqual(asset.depreciation_line_ids[4].amount, 160.00, places=2) # quarterly depreciation asset = self.asset_model.create( { "name": "test asset", "profile_id": self.car5y.id, "purchase_value": 1000, "salvage_value": 0, "date_start": time.strftime("%Y-07-07"), "method_time": "year", "method": "degr-linear", "method_progress_factor": 0.40, "method_number": 5, "method_period": "quarter", "prorata": False, } ) asset.compute_depreciation_board() asset.refresh() # check values in the depreciation board self.assertEqual(len(asset.depreciation_line_ids), 15) # lines prior to asset start period are grouped in the first entry self.assertAlmostEqual(asset.depreciation_line_ids[1].amount, 300.00, places=2) self.assertAlmostEqual(asset.depreciation_line_ids[3].amount, 60.00, places=2) self.assertAlmostEqual(asset.depreciation_line_ids[7].amount, 50.00, places=2) self.assertAlmostEqual(asset.depreciation_line_ids[13].amount, 40.00, places=2) def test_06_degressive_limit(self): """Degressive with annual depreciation.""" asset = self.asset_model.create( { "name": "test asset", "profile_id": self.car5y.id, "purchase_value": 1000, "salvage_value": 100, "date_start": time.strftime("%Y-07-07"), "method_time": "year", "method": "degr-limit", "method_progress_factor": 0.40, "method_number": 5, "method_period": "year", "prorata": False, } ) asset.compute_depreciation_board() asset.refresh() # check values in the depreciation board self.assertEqual(len(asset.depreciation_line_ids), 6) self.assertAlmostEqual(asset.depreciation_line_ids[1].amount, 400.00, places=2) self.assertAlmostEqual(asset.depreciation_line_ids[2].amount, 240.00, places=2) self.assertAlmostEqual(asset.depreciation_line_ids[3].amount, 144.00, places=2) self.assertAlmostEqual(asset.depreciation_line_ids[4].amount, 86.40, places=2) self.assertAlmostEqual(asset.depreciation_line_ids[5].amount, 29.60, places=2) def test_07_linear_limit(self): """Degressive with annual depreciation.""" asset = self.asset_model.create( { "name": "test asset", "profile_id": self.car5y.id, "purchase_value": 1000, "salvage_value": 100, "date_start": time.strftime("%Y-07-07"), "method_time": "year", "method": "linear-limit", "method_number": 5, "method_period": "year", "prorata": False, } ) asset.compute_depreciation_board() asset.refresh() # check values in the depreciation board self.assertEqual(len(asset.depreciation_line_ids), 6) self.assertAlmostEqual(asset.depreciation_line_ids[1].amount, 200.00, places=2) self.assertAlmostEqual(asset.depreciation_line_ids[-1].amount, 100.00, places=2) def test_08_asset_removal(self): """Asset removal""" asset = self.asset_model.create( { "name": "test asset removal", "profile_id": self.car5y.id, "purchase_value": 5000, "salvage_value": 0, "date_start": "2019-01-01", "method_time": "year", "method_number": 5, "method_period": "quarter", "prorata": False, } ) asset.compute_depreciation_board() asset.validate() wiz_ctx = {"active_id": asset.id, "early_removal": True} wiz = self.remove_model.with_context(wiz_ctx).create( { "date_remove": "2019-01-31", "sale_value": 0.0, "posting_regime": "gain_loss_on_sale", "account_plus_value_id": self.company_data[ "default_account_revenue" ].id, "account_min_value_id": self.company_data["default_account_expense"].id, } ) wiz.remove() asset.refresh() self.assertEqual(len(asset.depreciation_line_ids), 3) self.assertAlmostEqual(asset.depreciation_line_ids[1].amount, 83.33, places=2) self.assertAlmostEqual(asset.depreciation_line_ids[2].amount, 4916.67, places=2) def test_09_asset_from_invoice(self): all_asset = self.env["account.asset"].search([]) invoice = self.invoice asset_profile = self.car5y asset_profile.asset_product_item = False self.assertTrue(len(invoice.invoice_line_ids) > 0) line = invoice.invoice_line_ids[0] self.assertTrue(line.price_unit > 0.0) move_form = Form(invoice) with move_form.invoice_line_ids.edit(0) as line_form: line_form.quantity = 2 line_form.asset_profile_id = asset_profile invoice = move_form.save() invoice.action_post() # get all asset after invoice validation current_asset = self.env["account.asset"].search([]) # get the new asset new_asset = current_asset - all_asset # check that a new asset is created self.assertEqual(len(new_asset), 1) # check that the new asset has the correct purchase value self.assertAlmostEqual( new_asset.purchase_value, line.price_unit * line.quantity, places=2 ) def test_10_asset_from_invoice_product_item(self): all_asset = self.env["account.asset"].search([]) invoice = self.invoice asset_profile = self.car5y asset_profile.asset_product_item = True self.assertTrue(len(invoice.invoice_line_ids) > 0) line = invoice.invoice_line_ids[0] self.assertTrue(line.price_unit > 0.0) line.quantity = 2 line.asset_profile_id = asset_profile self.assertEqual(len(invoice.invoice_line_ids), 2) invoice.action_post() # get all asset after invoice validation current_asset = self.env["account.asset"].search([]) # get the new asset new_asset = current_asset - all_asset # check that a new asset is created self.assertEqual(len(new_asset), 2) for asset in new_asset: # check that the new asset has the correct purchase value self.assertAlmostEqual(asset.purchase_value, line.price_unit, places=2) def test_11_assets_from_invoice(self): all_assets = self.env["account.asset"].search([]) ctx = dict(self.invoice_2._context) del ctx["default_move_type"] invoice = self.invoice_2.with_context(ctx) asset_profile = self.car5y asset_profile.asset_product_item = True # Compute depreciation lines on invoice validation asset_profile.open_asset = True self.assertTrue(len(invoice.invoice_line_ids) == 2) invoice.invoice_line_ids.write( {"quantity": 1, "asset_profile_id": asset_profile.id} ) invoice.action_post() # Retrieve all assets after invoice validation current_assets = self.env["account.asset"].search([]) # What are the new assets? new_assets = current_assets - all_assets self.assertEqual(len(new_assets), 2) for asset in new_assets: dlines = asset.depreciation_line_ids.filtered( lambda l: l.type == "depreciate" ) dlines = dlines.sorted(key=lambda l: l.line_date) self.assertAlmostEqual(dlines[0].depreciated_value, 0.0) self.assertAlmostEqual(dlines[-1].remaining_value, 0.0) def test_12_prorata_days_calc(self): """Prorata temporis depreciation with days calc option.""" asset = self.asset_model.create( { "name": "test asset", "profile_id": self.car5y.id, "purchase_value": 3333, "salvage_value": 0, "date_start": "2019-07-07", "method_time": "year", "method_number": 5, "method_period": "month", "prorata": True, "days_calc": True, "use_leap_years": False, } ) asset.compute_depreciation_board() asset.refresh() day_rate = 3333 / 1827 # 3333 / 1827 depreciation days for i in range(1, 10): self.assertAlmostEqual( asset.depreciation_line_ids[i].amount, asset.depreciation_line_ids[i].line_days * day_rate, places=2, ) # Last depreciation remaining self.assertAlmostEqual(asset.depreciation_line_ids[-1].amount, 11.05, places=2) def test_13_use_leap_year(self): # When you use the depreciation with years method and using lap years, # the depreciation amount is calculated as 10000 / 1826 days * 365 days # = yearly depreciation amount of 1998.90. # Then 1998.90 / 12 = 166.58 asset = self.asset_model.create( { "name": "test asset", "profile_id": self.car5y.id, "purchase_value": 10000, "salvage_value": 0, "date_start": time.strftime("2019-01-01"), "method_time": "year", "method_number": 5, "method_period": "month", "prorata": False, "days_calc": False, "use_leap_years": True, } ) asset.compute_depreciation_board() asset.refresh() for i in range(2, 11): self.assertAlmostEqual( asset.depreciation_line_ids[i].amount, 166.58, places=2 ) self.assertAlmostEqual( asset.depreciation_line_ids[13].depreciated_value, 1998.90, places=2 ) def test_14_not_use_leap_year(self): # When you run a depreciation with method = 'year' and no not use # lap years you divide 1000 / 5 years = 2000, then divided by 12 months # to get 166.67 per month, equal for all periods. asset = self.asset_model.create( { "name": "test asset", "profile_id": self.car5y.id, "purchase_value": 10000, "salvage_value": 0, "date_start": time.strftime("2019-01-01"), "method_time": "year", "method_number": 5, "method_period": "month", "prorata": False, "days_calc": False, "use_leap_years": False, } ) asset.compute_depreciation_board() asset.refresh() for _i in range(1, 11): self.assertAlmostEqual( asset.depreciation_line_ids[1].amount, 166.67, places=2 ) # In the last month of the fiscal year we compensate for the small # deviations if that is necessary. self.assertAlmostEqual(asset.depreciation_line_ids[12].amount, 166.63, places=2) def test_15_account_asset_group(self): """Group's name_get behaves differently depending on code and context""" group_fa = self.env["account.asset.group"].create( { "name": "Fixed Assets", "code": "FA", } ) group_tfa = self.env["account.asset.group"].create( { "name": "Tangible Fixed Assets", "code": "TFA", } ) # Groups are displayed by code (if any) plus name self.assertEqual( self.env["account.asset.group"].name_search("FA"), [(group_fa.id, "FA Fixed Assets")], ) # Groups with code are shown by code in list views self.assertEqual( self.env["account.asset.group"] .with_context(params={"view_type": "list"}) .name_search("FA"), [(group_fa.id, "FA")], ) self.assertEqual( self.env["account.asset.group"].name_search("TFA"), [(group_tfa.id, "TFA Tangible Fixed Assets")], ) group_tfa.code = False group_fa.code = False self.assertEqual(group_fa.name_get(), [(group_fa.id, "Fixed Assets")]) # Groups without code are shown by truncated name in lists self.assertEqual( group_tfa.name_get(), [(group_tfa.id, "Tangible Fixed Assets")] ) self.assertEqual( group_tfa.with_context(params={"view_type": "list"}).name_get(), [(group_tfa.id, "Tangible Fixed A...")], ) self.assertFalse(self.env["account.asset.group"].name_search("stessA dexiF")) def test_16_use_number_of_depreciations(self): # When you run a depreciation with method = 'number' profile = self.car5y profile.method_time = "number" asset = self.asset_model.create( { "name": "test asset", "profile_id": profile.id, "purchase_value": 10000, "salvage_value": 0, "date_start": time.strftime("2019-01-01"), "method_time": "year", "method_number": 5, "method_period": "month", "prorata": False, "days_calc": False, "use_leap_years": False, } ) asset.compute_depreciation_board() asset.refresh() for _i in range(1, 11): self.assertAlmostEqual( asset.depreciation_line_ids[1].amount, 166.67, places=2 ) # In the last month of the fiscal year we compensate for the small # deviations if that is necessary. self.assertAlmostEqual(asset.depreciation_line_ids[12].amount, 166.63, places=2)