diff --git a/addons/at_accounting/tests/test_account_asset.py b/addons/at_accounting/tests/test_account_asset.py new file mode 100644 index 0000000..8d2d622 --- /dev/null +++ b/addons/at_accounting/tests/test_account_asset.py @@ -0,0 +1,3037 @@ +# -*- coding: utf-8 -*- + +import time + +from dateutil.relativedelta import relativedelta +from odoo import fields, Command +from odoo.exceptions import UserError, MissingError +from odoo.tests import Form, tagged, freeze_time +from odoo.addons.at_accounting.tests.common import TestAccountReportsCommon + + +@freeze_time('2021-07-01') +@tagged('post_install', '-at_install') +class TestAccountAsset(TestAccountReportsCommon): + + @classmethod + def setUpClass(cls): + super(TestAccountAsset, cls).setUpClass() + today = fields.Date.today() + cls.truck = cls.env['account.asset'].create({ + 'account_asset_id': cls.company_data['default_account_assets'].id, + 'account_depreciation_id': cls.company_data['default_account_assets'].copy().id, + 'account_depreciation_expense_id': cls.company_data['default_account_expense'].id, + 'journal_id': cls.company_data['default_journal_misc'].id, + 'name': 'truck', + 'acquisition_date': today + relativedelta(years=-6, months=-6), + 'original_value': 10000, + 'salvage_value': 2500, + 'method_number': 10, + 'method_period': '12', + 'method': 'linear', + }) + cls.truck.validate() + cls.env['account.move']._autopost_draft_entries() + + cls.account_asset_model_fixedassets = cls.env['account.asset'].create({ + 'account_depreciation_id': cls.company_data['default_account_assets'].copy().id, + 'account_depreciation_expense_id': cls.company_data['default_account_expense'].id, + 'account_asset_id': cls.company_data['default_account_assets'].id, + 'journal_id': cls.company_data['default_journal_purchase'].id, + 'name': 'Hardware - 3 Years', + 'method_number': 3, + 'method_period': '12', + 'state': 'model', + }) + + + cls.closing_invoice = cls.env['account.move'].create({ + 'move_type': 'out_invoice', + 'invoice_line_ids': [(0, 0, {'price_unit': 100})] + }) + + cls.env.company.loss_account_id = cls.company_data['default_account_expense'].copy() + cls.env.company.gain_account_id = cls.company_data['default_account_revenue'].copy() + cls.assert_counterpart_account_id = cls.company_data['default_account_expense'].copy().id + + cls.env.user.groups_id += cls.env.ref('analytic.group_analytic_accounting') + analytic_plan = cls.env['account.analytic.plan'].create({ + 'name': "Default Plan", + }) + cls.analytic_account = cls.env['account.analytic.account'].create({ + 'name': "Test Account", + 'plan_id': analytic_plan.id, + }) + + def update_form_values(self, asset_form): + for i in range(len(asset_form.depreciation_move_ids)): + with asset_form.depreciation_move_ids.edit(i) as line_edit: + line_edit.asset_remaining_value + + def test_account_asset_no_tax(self): + self.account_asset_model_fixedassets.account_depreciation_expense_id.tax_ids = self.tax_purchase_a + CEO_car = self.env['account.asset'].create({ + 'salvage_value': 2000.0, + 'state': 'open', + 'method_period': '12', + 'method_number': 5, + 'name': "CEO's Car", + 'original_value': 12000.0, + 'model_id': self.account_asset_model_fixedassets.id, + }) + CEO_car._onchange_model_id() + CEO_car.prorata_computation_type = 'constant_periods' + CEO_car.method_number = 5 + + # In order to test the process of Account Asset, I perform a action to confirm Account Asset. + CEO_car.validate() + + self.assertFalse(any(CEO_car.depreciation_move_ids.line_ids.mapped('tax_line_id'))) + + def test_00_account_asset(self): + """Test the lifecycle of an asset""" + CEO_car = self.env['account.asset'].create({ + 'salvage_value': 2000.0, + 'state': 'open', + 'method_period': '12', + 'method_number': 5, + 'name': "CEO's Car", + 'original_value': 12000.0, + 'model_id': self.account_asset_model_fixedassets.id, + }) + CEO_car._onchange_model_id() + CEO_car.prorata_computation_type = 'constant_periods' + CEO_car.method_number = 5 + + # In order to test the process of Account Asset, I perform a action to confirm Account Asset. + CEO_car.validate() + + # TOFIX: the method validate() makes the field account.asset.asset_type + # dirty, but this field has to be flushed in CEO_car's environment. + # This is because the field 'asset_type' is stored, computed and + # context-dependent, which explains why its value must be retrieved + # from the right environment. + CEO_car.flush_recordset() + + # I check Asset is now in Open state. + self.assertEqual(CEO_car.state, 'open', + 'Asset should be in Open state') + + # I compute depreciation lines for asset of CEOs Car. + self.assertEqual(CEO_car.method_number + 1, len(CEO_car.depreciation_move_ids), + 'Depreciation lines not created correctly') + + # Check that auto_post is set on the entries, in the future, and we cannot post them. + self.assertTrue(all(CEO_car.depreciation_move_ids.mapped(lambda m: m.auto_post != 'no'))) + with self.assertRaises(UserError): + CEO_car.depreciation_move_ids.action_post() + + # I Check that After creating all the moves of depreciation lines the state "Running". + CEO_car.depreciation_move_ids.write({'auto_post': 'no'}) + CEO_car.depreciation_move_ids.action_post() + self.assertEqual(CEO_car.state, 'open', + 'State of asset should be runing') + self.assertRecordValues(CEO_car, [{ + 'original_value': 12000, + 'book_value': 2000, + 'value_residual': 0, + 'salvage_value': 2000, + }]) + + self.assertRecordValues(CEO_car.depreciation_move_ids.sorted(lambda l: l.date), [{ + 'amount_total': 1000, + 'asset_remaining_value': 9000, + }, { + 'amount_total': 2000, + 'asset_remaining_value': 7000, + }, { + 'amount_total': 2000, + 'asset_remaining_value': 5000, + }, { + 'amount_total': 2000, + 'asset_remaining_value': 3000, + }, { + 'amount_total': 2000, + 'asset_remaining_value': 1000, + }, { + 'amount_total': 1000, + 'asset_remaining_value': 0, + }]) + + # Revert posted entries in order to be able to close + CEO_car.depreciation_move_ids._reverse_moves(cancel=True) + self.assertRecordValues(CEO_car, [{ + 'original_value': 12000, + 'book_value': 12000, + 'value_residual': 10000, + 'salvage_value': 2000, + }]) + reversed_moves_values = [{ + 'amount_total': 1000, + 'asset_remaining_value': 11000, + 'state': 'posted', + }, { + 'amount_total': 2000, + 'asset_remaining_value': 13000, + 'state': 'posted', + }, { + 'amount_total': 2000, + 'asset_remaining_value': 15000, + 'state': 'posted', + }, { + 'amount_total': 2000, + 'asset_remaining_value': 17000, + 'state': 'posted', + }, { + 'amount_total': 2000, + 'asset_remaining_value': 19000, + 'state': 'posted', + }, { + 'amount_total': 1000, + 'asset_remaining_value': 20000, + 'state': 'posted', + }, { + 'amount_total': 1000, + 'asset_remaining_value': 19000, + 'state': 'posted', + }, { + 'amount_total': 2000, + 'asset_remaining_value': 17000, + 'state': 'posted', + }, { + 'amount_total': 2000, + 'asset_remaining_value': 15000, + 'state': 'posted', + }, { + 'amount_total': 2000, + 'asset_remaining_value': 13000, + 'state': 'posted', + }, { + 'amount_total': 2000, + 'asset_remaining_value': 11000, + 'state': 'posted', + }, { + 'amount_total': 1000, + 'asset_remaining_value': 10000, + 'state': 'posted', + }, { + 'amount_total': 10000, + 'asset_remaining_value': 0, + 'state': 'draft', + }] + + self.assertRecordValues(CEO_car.depreciation_move_ids.sorted(lambda l: l.date), reversed_moves_values) + self.assertRecordValues(CEO_car.depreciation_move_ids.filtered(lambda l: l.state == 'draft').line_ids, [{ + 'debit': 0, + 'credit': 10000, + 'account_id': CEO_car.account_depreciation_id.id, + }, { + 'debit': 10000, + 'credit': 0, + 'account_id': CEO_car.account_depreciation_expense_id.id, + }]) + + # Close + CEO_car.set_to_close(self.closing_invoice.invoice_line_ids, date=fields.Date.today() + relativedelta(days=-1)) + self.assertRecordValues(CEO_car, [{ + 'original_value': 12000, + 'book_value': 12000, + 'value_residual': 10000, + 'salvage_value': 2000, + }]) + self.assertRecordValues(CEO_car.depreciation_move_ids.sorted(lambda l: (l.date, l.id)), [{ + 'amount_total': 12000, + 'asset_remaining_value': 0, + 'state': 'draft', + }, { + 'amount_total': 1000, + 'asset_remaining_value': 1000, + 'state': 'posted', + }, { + 'amount_total': 2000, + 'asset_remaining_value': 3000, + 'state': 'posted', + }, { + 'amount_total': 2000, + 'asset_remaining_value': 5000, + 'state': 'posted', + }, { + 'amount_total': 2000, + 'asset_remaining_value': 7000, + 'state': 'posted', + }, { + 'amount_total': 2000, + 'asset_remaining_value': 9000, + 'state': 'posted', + }, { + 'amount_total': 1000, + 'asset_remaining_value': 10000, + 'state': 'posted', + }, { + 'amount_total': 1000, + 'asset_remaining_value': 9000, + 'state': 'posted', + }, { + 'amount_total': 2000, + 'asset_remaining_value': 7000, + 'state': 'posted', + }, { + 'amount_total': 2000, + 'asset_remaining_value': 5000, + 'state': 'posted', + }, { + 'amount_total': 2000, + 'asset_remaining_value': 3000, + 'state': 'posted', + }, { + 'amount_total': 2000, + 'asset_remaining_value': 1000, + 'state': 'posted', + }, { + 'amount_total': 1000, + 'asset_remaining_value': 0, + 'state': 'posted', + }]) + closing_move = CEO_car.depreciation_move_ids.filtered(lambda l: l.state == 'draft') + self.assertRecordValues(closing_move.line_ids, [{ + 'debit': 0, + 'credit': 12000, + 'account_id': CEO_car.account_asset_id.id, + }, { + 'debit': 0, + 'credit': 0, + 'account_id': CEO_car.account_depreciation_id.id, + }, { + 'debit': 100, + 'credit': 0, + 'account_id': self.closing_invoice.invoice_line_ids.account_id.id, + }, { + 'debit': 11900, + 'credit': 0, + 'account_id': self.env.company.loss_account_id.id, + }]) + closing_move.action_post() + self.assertRecordValues(CEO_car, [{ + 'original_value': 12000, + 'book_value': 0, + 'value_residual': 0, + 'salvage_value': 2000, + }]) + + def test_00_account_asset_new(self): + """Test the lifecycle of an asset""" + CEO_car = self.env['account.asset'].create({ + 'salvage_value': 2000.0, + 'state': 'open', + 'method_period': '12', + 'method_number': 5, + 'name': "CEO's Car", + 'original_value': 12000.0, + 'model_id': self.account_asset_model_fixedassets.id, + }) + CEO_car._onchange_model_id() + CEO_car.prorata_computation_type = 'constant_periods' + CEO_car.method_number = 5 + + # In order to test the process of Account Asset, I perform a action to confirm Account Asset. + CEO_car.validate() + + # I Check that After creating all the moves of depreciation lines the state of the asset is "Running". + CEO_car.depreciation_move_ids.write({'auto_post': 'no'}) + CEO_car.depreciation_move_ids.action_post() + self.assertEqual(CEO_car.state, 'open', + 'State of the asset should be running') + self.assertRecordValues(CEO_car, [{ + 'original_value': 12000, + 'book_value': 2000, + 'value_residual': 0, + 'salvage_value': 2000, + }]) + self.assertRecordValues(CEO_car.depreciation_move_ids.sorted(lambda l: l.date), [{ + 'amount_total': 1000, + 'asset_remaining_value': 9000, + }, { + 'amount_total': 2000, + 'asset_remaining_value': 7000, + }, { + 'amount_total': 2000, + 'asset_remaining_value': 5000, + }, { + 'amount_total': 2000, + 'asset_remaining_value': 3000, + }, { + 'amount_total': 2000, + 'asset_remaining_value': 1000, + }, { + 'amount_total': 1000, + 'asset_remaining_value': 0, + }]) + + # Close + CEO_car.set_to_close(self.closing_invoice.invoice_line_ids, date=fields.Date.today() + relativedelta(days=30)) + self.assertRecordValues(CEO_car, [{ + 'original_value': 12000, + 'book_value': 12000, + 'value_residual': 10000, + 'salvage_value': 2000, + }]) + self.assertRecordValues(CEO_car.depreciation_move_ids.sorted(lambda l: (l.date, l.id)), [{ + 'amount_total': 166.67, + 'asset_remaining_value': 9833.33, + 'state': 'draft', + }, { + 'amount_total': 12000, + 'asset_remaining_value': 0, + 'state': 'draft', + }]) + closing_move = max(CEO_car.depreciation_move_ids, key=lambda m: (m.date, m.id)) + self.assertRecordValues(closing_move, [{ + 'date': fields.Date.today() + relativedelta(days=30), + }]) + self.assertRecordValues(closing_move.line_ids, [{ + 'debit': 0, + 'credit': 12000, + 'account_id': CEO_car.account_asset_id.id, + }, { + 'debit': 166.67, + 'credit': 0, + 'account_id': CEO_car.account_depreciation_id.id, + }, { + 'debit': 100, + 'credit': 0, + 'account_id': self.closing_invoice.invoice_line_ids.account_id.id, + }, { + 'debit': 11733.33, + 'credit': 0, + 'account_id': self.env.company.loss_account_id.id, + }]) + CEO_car.depreciation_move_ids.auto_post = 'no' + CEO_car.depreciation_move_ids.action_post() + self.assertRecordValues(CEO_car, [{ + 'original_value': 12000, + 'book_value': 0, + 'value_residual': 0, + 'salvage_value': 2000, + 'state': 'close', + }]) + + def test_01_account_asset(self): + """ Test if an an asset is created when an invoice is validated with an + item on an account for generating entries. + """ + account_asset_model = self.env['account.asset'].create({ + 'account_depreciation_id': self.company_data['default_account_assets'].id, + 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, + 'journal_id': self.company_data['default_journal_misc'].id, + 'name': 'Typical car - 3 Years', + 'method_number': 3, + 'method_period': '12', + 'prorata_computation_type': 'daily_computation', + 'state': 'model', + }) + + # The account needs a default model for the invoice to validate the revenue + self.company_data['default_account_assets'].create_asset = 'validate' + self.company_data['default_account_assets'].asset_model_ids = account_asset_model + + invoice = self.env['account.move'].create({ + 'move_type': 'in_invoice', + 'partner_id': self.env['res.partner'].create({'name': 'Res Partner 12'}).id, + 'invoice_date': '2020-12-31', + 'invoice_line_ids': [(0, 0, { + 'name': 'Very little red car', + 'account_id': self.company_data['default_account_assets'].id, + 'price_unit': 450, + 'quantity': 1, + })], + }) + invoice.action_post() + + asset = invoice.asset_ids + self.assertEqual(len(asset), 1, 'One and only one asset should have been created from invoice.') + + self.assertTrue(asset.state == 'open', + 'Asset should be in Open state') + first_invoice_line = invoice.invoice_line_ids[0] + self.assertEqual(asset.original_value, first_invoice_line.price_subtotal, + 'Asset value is not same as invoice line.') + + # I check data in move line and depreciation line. + first_depreciation_line = asset.depreciation_move_ids.sorted(lambda r: r.id)[0] + self.assertAlmostEqual(first_depreciation_line.asset_remaining_value, asset.original_value - first_depreciation_line.amount_total, + msg='Remaining value is incorrect.') + self.assertAlmostEqual(first_depreciation_line.asset_depreciated_value, first_depreciation_line.amount_total, + msg='Depreciated value is incorrect.') + + # I check next installment date. + last_depreciation_date = first_depreciation_line.date + installment_date = last_depreciation_date + relativedelta(months=+int(asset.method_period)) + self.assertEqual(asset.depreciation_move_ids.sorted(lambda r: r.id)[1].date, installment_date, + 'Installment date is incorrect.') + + def test_02_account_asset(self): + """Test the lifecycle of an asset""" + CEO_car = self.env['account.asset'].create({ + 'salvage_value': 2000.0, + 'state': 'open', + 'method_period': '12', + 'method_number': 5, + 'name': "CEO's Car", + 'original_value': 12000.0, + 'model_id': self.account_asset_model_fixedassets.id, + 'acquisition_date': '2010-01-31', + 'already_depreciated_amount_import': 10000.0, + }) + CEO_car._onchange_model_id() + + CEO_car.validate() + self.assertRecordValues(CEO_car, [{ + 'original_value': 12000, + 'book_value': 2000, + 'value_residual': 0, + 'salvage_value': 2000, + }]) + self.assertFalse(CEO_car.depreciation_move_ids) + CEO_car.set_to_close(self.closing_invoice.invoice_line_ids) + self.assertRecordValues(CEO_car, [{ + 'original_value': 12000, + 'book_value': 2000, + 'value_residual': 0, + 'salvage_value': 2000, + }]) + closing_move = CEO_car.depreciation_move_ids.filtered(lambda l: l.state == 'draft') + self.assertRecordValues(closing_move.line_ids, [{ + 'debit': 0, + 'credit': 12000, + 'account_id': CEO_car.account_asset_id.id, + }, { + 'debit': 10000, + 'credit': 0, + 'account_id': CEO_car.account_depreciation_id.id, + }, { + 'debit': 100, + 'credit': 0, + 'account_id': self.closing_invoice.invoice_line_ids.account_id.id, + }, { + 'debit': 1900, + 'credit': 0, + 'account_id': CEO_car.company_id.loss_account_id.id, + }]) + closing_move.action_post() + self.assertRecordValues(CEO_car, [{ + 'original_value': 12000, + 'book_value': 0, + 'value_residual': 0, + 'salvage_value': 2000, + }]) + + def test_03_account_asset(self): + """Test the salvage of an asset with gain""" + CEO_car = self.env['account.asset'].create({ + 'salvage_value': 0, + 'state': 'open', + 'method_period': '12', + 'method_number': 5, + 'name': "CEO's Car", + 'original_value': 12000.0, + 'model_id': self.account_asset_model_fixedassets.id, + 'acquisition_date': '2010-01-31', + 'already_depreciated_amount_import': 12000.0, + }) + CEO_car._onchange_model_id() + + CEO_car.validate() + self.assertRecordValues(CEO_car, [{ + 'original_value': 12000, + 'book_value': 0, + 'value_residual': 0, + 'salvage_value': 0, + }]) + self.assertFalse(CEO_car.depreciation_move_ids) + CEO_car.set_to_close(self.closing_invoice.invoice_line_ids) + self.assertRecordValues(CEO_car, [{ + 'original_value': 12000, + 'book_value': 0, + 'value_residual': 0, + 'salvage_value': 0, + }]) + closing_move = CEO_car.depreciation_move_ids.filtered(lambda l: l.state == 'draft') + self.assertRecordValues(closing_move.line_ids, [{ + 'debit': 0, + 'credit': 12000, + 'account_id': CEO_car.account_asset_id.id, + }, { + 'debit': 12000, + 'credit': 0, + 'account_id': CEO_car.account_depreciation_id.id, + }, { + 'debit': 100, + 'credit': 0, + 'account_id': self.closing_invoice.invoice_line_ids.account_id.id, + }, { + 'debit': 0, + 'credit': 100, + 'account_id': CEO_car.company_id.gain_account_id.id, + }]) + closing_move.action_post() + self.assertRecordValues(CEO_car, [{ + 'original_value': 12000, + 'book_value': 0, + 'value_residual': 0, + 'salvage_value': 0, + }]) + + def test_04_account_asset(self): + """Test the salvage of an asset with gain""" + CEO_car = self.env['account.asset'].create({ + 'salvage_value': 0, + 'state': 'open', + 'method_period': '12', + 'method_number': 5, + 'name': "CEO's Car", + 'original_value': 800.0, + 'model_id': self.account_asset_model_fixedassets.id, + 'acquisition_date': '2021-01-01', + 'already_depreciated_amount_import': 300.0, + }) + CEO_car._onchange_model_id() + CEO_car.method_number = 5 + + CEO_car.validate() + self.assertRecordValues(CEO_car, [{ + 'original_value': 800, + 'book_value': 500, + 'value_residual': 500, + 'salvage_value': 0, + }]) + self.assertEqual(len(CEO_car.depreciation_move_ids), 4) + CEO_car.set_to_close(self.closing_invoice.invoice_line_ids, date=fields.Date.today() + relativedelta(months=-6, days=-1)) + self.assertRecordValues(CEO_car, [{ + 'original_value': 800, + 'book_value': 500, + 'value_residual': 500, + 'salvage_value': 0, + }]) + closing_move = CEO_car.depreciation_move_ids.filtered(lambda l: l.state == 'draft') + self.assertRecordValues(closing_move.line_ids, [{ + 'debit': 0, + 'credit': 800, + 'account_id': CEO_car.account_asset_id.id, + }, { + 'debit': 300, + 'credit': 0, + 'account_id': CEO_car.account_depreciation_id.id, + }, { + 'debit': 100, + 'credit': 0, + 'account_id': self.closing_invoice.invoice_line_ids.account_id.id, + }, { + 'debit': 400, + 'credit': 0, + 'account_id': CEO_car.company_id.loss_account_id.id, + }]) + closing_move.action_post() + self.assertRecordValues(CEO_car, [{ + 'original_value': 800, + 'book_value': 0, + 'value_residual': 0, + 'salvage_value': 0, + }]) + + def test_05_account_asset(self): + """Test the salvage of an asset with gain""" + CEO_car = self.env['account.asset'].create({ + 'salvage_value': 0, + 'state': 'open', + 'method_period': '12', + 'method_number': 5, + 'name': "CEO's Car", + 'original_value': 1000.0, + 'model_id': self.account_asset_model_fixedassets.id, + 'acquisition_date': '2020-01-01', + }) + CEO_car._onchange_model_id() + CEO_car.method_number = 5 + CEO_car.account_depreciation_id = CEO_car.account_asset_id + + CEO_car.validate() + self.assertRecordValues(CEO_car, [{ + 'original_value': 1000, + 'book_value': 800, + 'value_residual': 800, + 'salvage_value': 0, + }]) + self.assertEqual(len(CEO_car.depreciation_move_ids), 5) + CEO_car.set_to_close(self.env['account.move.line'], date=fields.Date.today() + relativedelta(days=-1)) + self.assertRecordValues(CEO_car, [{ + 'original_value': 1000, + 'book_value': 700, + 'value_residual': 700, + 'salvage_value': 0, + }]) + closing_move = CEO_car.depreciation_move_ids.filtered(lambda l: l.state == 'draft') + self.assertRecordValues(closing_move.line_ids, [{ + 'debit': 0, + 'credit': 1000, + 'account_id': CEO_car.account_asset_id.id, + }, { + 'debit': 300, + 'credit': 0, + 'account_id': CEO_car.account_depreciation_id.id, + }, { + 'debit': 700, + 'credit': 0, + 'account_id': CEO_car.company_id.loss_account_id.id, + }]) + closing_move.action_post() + self.assertRecordValues(CEO_car, [{ + 'original_value': 1000, + 'book_value': 0, + 'value_residual': 0, + 'salvage_value': 0, + }]) + + def test_06_account_asset(self): + """Test the correct computation of asset amounts""" + asset_account = self.env['account.account'].create({ + "name": "test_06_account_asset", + "code": "test.06.account.asset", + "account_type": 'asset_non_current', + "create_asset": "no", + "multiple_assets_per_line": True, + }) + + CEO_car = self.env['account.asset'].create({ + 'salvage_value': 0, + 'state': 'draft', + 'method_period': '12', + 'method_number': 4, + 'name': "CEO's Car", + 'original_value': 1000.0, + 'acquisition_date': fields.Date.today() - relativedelta(years=3), + 'account_asset_id': asset_account.id, + 'account_depreciation_id': self.company_data['default_account_assets'].copy().id, + 'account_depreciation_expense_id': asset_account.id, + 'journal_id': self.company_data['default_journal_misc'].id, + 'prorata_computation_type': 'none', + }) + + CEO_car.validate() + posted_entries = len(CEO_car.depreciation_move_ids.filtered(lambda x: x.state == 'posted')) + self.assertEqual(posted_entries, 3) + + self.assertRecordValues(CEO_car, [{ + 'original_value': 1000, + 'book_value': 250, + 'value_residual': 250, + 'salvage_value': 0, + }]) + + def test_account_asset_cancel(self): + """Test the cancellation of an asset""" + today = fields.Date.today() + CEO_car = self.env['account.asset'].create({ + 'salvage_value': 2000.0, + 'state': 'open', + 'method_period': '12', + 'method_number': 5, + 'name': "CEO's Car", + 'original_value': 12000.0, + 'model_id': self.account_asset_model_fixedassets.id, + 'acquisition_date': today + relativedelta(years=-3, month=1, day=1), + }) + CEO_car._onchange_model_id() + CEO_car.method_number = 5 + CEO_car.validate() + + self.assertRecordValues(CEO_car, [{ + 'original_value': 12000, + 'book_value': 6000, + 'value_residual': 4000, + 'salvage_value': 2000, + }]) + CEO_car.set_to_cancelled() + + self.assertEqual(CEO_car.state, 'cancelled') + self.assertFalse(CEO_car.depreciation_move_ids) + + # Hashed journals should reverse entries instead of deleting + Hashed_car = CEO_car.copy() + Hashed_car.write({ + 'original_value': 12000.0, + 'method_number': 5, + 'name': "Hashed Car", + 'journal_id': CEO_car.journal_id.copy().id, + 'acquisition_date': today + relativedelta(years=-3, month=1, day=1), + }) + Hashed_car.journal_id.restrict_mode_hash_table = True + Hashed_car.validate() + self.assertTrue(False not in Hashed_car.depreciation_move_ids[:3].mapped('inalterable_hash')) + + for i in range(0, 4): + self.assertFalse(Hashed_car.depreciation_move_ids[i].reversal_move_ids) + + Hashed_car.set_to_cancelled() + + self.assertEqual(Hashed_car.state, 'cancelled') + for i in range(0, 2): + self.assertTrue(Hashed_car.depreciation_move_ids[i].reversal_move_ids.id > 0 or Hashed_car.depreciation_move_ids[i].reversed_entry_id.id > 0) + + # The depreciation schedule report should not contain cancelled assets + report = self.env.ref('at_accounting.assets_report') + options = self._generate_options(report, today + relativedelta(years=-6, month=1, day=1), today + relativedelta(years=+4, month=12, day=31)) + lines = report._get_lines({**options, **{'unfold_all': False, 'all_entries': True}}) + assets_in_report = [x['name'] for x in lines[:-1]] + + self.assertNotIn(CEO_car.name, assets_in_report) + self.assertNotIn(Hashed_car.name, assets_in_report) + + # When a lock date is applied, only the moves before the date are reversed, others are deleted + Locked_car = CEO_car.copy() + Locked_car.write({ + 'original_value': 12000.0, + 'method_number': 10, + 'name': "Locked Car", + 'acquisition_date': today + relativedelta(years=-3, month=1, day=1), + }) + Locked_car.validate() + Locked_car.company_id.fiscalyear_lock_date = today + relativedelta(years=-1) + + self.assertEqual(len(Locked_car.depreciation_move_ids), 10) + Locked_car.set_to_cancelled() + self.assertRecordValues(Locked_car, [{ + 'state': 'cancelled', + 'book_value': 12000.0, + 'value_residual': 10000, + 'salvage_value': 2000, + }]) + self.assertEqual(len(Locked_car.depreciation_move_ids), 4) + for depreciation in Locked_car.depreciation_move_ids: + self.assertTrue(depreciation.reversal_move_ids or depreciation.reversed_entry_id) + + + def test_asset_form(self): + """Test the form view of assets""" + asset_form = Form(self.env['account.asset']) + asset_form.name = "Test Asset" + asset_form.original_value = 10000 + asset_form.account_depreciation_id = self.company_data['default_account_assets'] + asset_form.account_depreciation_expense_id = self.company_data['default_account_expense'] + asset_form.journal_id = self.company_data['default_journal_misc'] + asset_form.prorata_computation_type = 'none' + asset = asset_form.save() + asset.validate() + + # Test that the depreciations are created upon validation of the asset according to the default values + self.assertEqual(len(asset.depreciation_move_ids), 5) + for move in asset.depreciation_move_ids: + self.assertEqual(move.amount_total, 2000) + + # Test that we cannot validate an asset with non zero remaining value of the last depreciation line + asset_form = Form(asset) + with self.assertRaises(UserError): + with self.cr.savepoint(): + with asset_form.depreciation_move_ids.edit(4) as line_edit: + line_edit.depreciation_value = 1000.0 + asset_form.save() + + # ... but we can with a zero remaining value on the last line. + asset_form = Form(asset) + with asset_form.depreciation_move_ids.edit(4) as line_edit: + line_edit.depreciation_value = 1000.0 + with asset_form.depreciation_move_ids.edit(3) as line_edit: + line_edit.depreciation_value = 3000.0 + self.update_form_values(asset_form) + asset_form.save() + + def test_asset_from_entry_line_form(self): + """Test that the asset is correcly created from a move line""" + + move_ids = self.env['account.move'].create([{ + 'ref': 'line1', + 'line_ids': [ + (0, 0, { + 'account_id': self.company_data['default_account_expense'].id, + 'debit': 300, + 'name': 'Furniture', + }), + (0, 0, { + 'account_id': self.company_data['default_account_assets'].id, + 'credit': 300, + }), + ] + }, { + 'ref': 'line2', + 'line_ids': [ + (0, 0, { + 'account_id': self.company_data['default_account_expense'].id, + 'debit': 600, + 'name': 'Furniture too', + }), + (0, 0, { + 'account_id': self.company_data['default_account_assets'].id, + 'credit': 600, + }), + ] + }, + ]) + move_ids.action_post() + move_line_ids = move_ids.mapped('line_ids').filtered(lambda x: x.debit) + + asset_form = Form(self.env['account.asset'].with_context(default_original_move_line_ids=move_line_ids.ids)) + asset_form.original_move_line_ids = move_line_ids + asset_form.account_depreciation_expense_id = self.company_data['default_account_expense'] + + asset = asset_form.save() + self.assertEqual(asset.value_residual, 900.0) + self.assertIn(asset.name, ['Furniture', 'Furniture too']) + self.assertEqual(asset.journal_id.type, 'general') + self.assertEqual(asset.account_asset_id, self.company_data['default_account_expense']) + self.assertEqual(asset.account_depreciation_id, self.company_data['default_account_expense']) + self.assertEqual(asset.account_depreciation_expense_id, self.company_data['default_account_expense']) + self.assertEqual(asset.acquisition_date, min(move_ids.mapped('date'))) + + def test_asset_from_bill_move_line_form(self): + """Test that the asset is correcly created from a move line""" + + move_ids = self.env['account.move'].create([{ + 'move_type': 'in_invoice', + 'partner_id': self.partner_a.id, + 'ref': 'line1', + 'date': '2020-06-01', + 'invoice_date': '2020-06-15', + 'invoice_line_ids': [ + Command.create({ + 'account_id': self.company_data['default_account_expense'].id, + 'price_unit': 300, + 'name': 'Furniture', + 'tax_ids': [], + }), + ] + }, { + 'move_type': 'in_invoice', + 'partner_id': self.partner_a.id, + 'ref': 'line2', + 'date': '2020-06-01', + 'invoice_date': '2020-06-14', + 'invoice_line_ids': [ + Command.create({ + 'account_id': self.company_data['default_account_expense'].id, + 'price_unit': 600, + 'name': 'Furniture too', + 'tax_ids': [], + }), + ] + }, + ]) + move_ids.action_post() + move_line_ids = move_ids.mapped('line_ids').filtered(lambda x: x.debit) + + asset_form = Form(self.env['account.asset'].with_context(default_original_move_line_ids=move_line_ids.ids)) + asset_form.original_move_line_ids = move_line_ids + asset_form.account_depreciation_expense_id = self.company_data['default_account_expense'] + + asset = asset_form.save() + self.assertEqual(asset.value_residual, 900.0) + self.assertRecordValues(asset, [{ + 'name': 'Furniture', + 'account_asset_id': self.company_data['default_account_expense'].id, + 'account_depreciation_id': self.company_data['default_account_expense'].id, + 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, + 'acquisition_date': min(move_ids.mapped('invoice_date')), + }]) + + def test_asset_from_bill_move_line_form_multicurrency(self): + """Test that the asset is correcly created from a move line using a foreign currency""" + + asset_account = self.company_data['default_account_assets'] + non_deductible_tax = self.env['account.tax'].create({ + 'name': 'Non-deductible Tax', + 'amount': 21, + 'amount_type': 'percent', + 'type_tax_use': 'purchase', + 'invoice_repartition_line_ids': [ + Command.create({'repartition_type': 'base'}), + Command.create({ + 'factor_percent': 50, + 'repartition_type': 'tax', + 'use_in_tax_closing': False + }), + Command.create({ + 'factor_percent': 50, + 'repartition_type': 'tax', + 'use_in_tax_closing': True + }), + ], + 'refund_repartition_line_ids': [ + Command.create({'repartition_type': 'base'}), + Command.create({ + 'factor_percent': 50, + 'repartition_type': 'tax', + 'use_in_tax_closing': False + }), + Command.create({ + 'factor_percent': 50, + 'repartition_type': 'tax', + 'use_in_tax_closing': True + }), + ], + }) + asset_account.tax_ids = non_deductible_tax + + asset_account.create_asset = 'no' + asset_account.multiple_assets_per_line = False + + vendor_bill = self.env['account.move'].create({ + 'move_type': 'in_invoice', + 'currency_id': self.other_currency.id, + 'invoice_date': '2020-01-01', + 'partner_id': self.partner_a.id, + 'invoice_line_ids': [ + Command.create({ + 'account_id': asset_account.id, + 'currency_id': self.other_currency.id, + 'name': 'Asus Laptop', + 'price_unit': 1000.0, + 'quantity': 1, + 'tax_ids': [Command.set(non_deductible_tax.ids)] + }), + Command.create({ + 'account_id': asset_account.id, + 'currency_id': self.other_currency.id, + 'name': 'Lenovo Laptop', + 'price_unit': 500.0, + 'quantity': 1, + 'tax_ids': [Command.set(non_deductible_tax.ids)] + }), + ], + }) + vendor_bill.action_post() + self.env.flush_all() + + move_line_ids = vendor_bill.mapped('line_ids').filtered(lambda x: 'Laptop' in x.name) + asset_form = Form(self.env['account.asset'].with_context( + default_original_move_line_ids=move_line_ids.ids, + asset_type='purchase' + )) + asset_form.original_move_line_ids = move_line_ids + asset_form.account_depreciation_expense_id = self.company_data['default_account_expense'] + + new_assets = asset_form.save() + self.assertEqual(len(new_assets), 1) + self.assertEqual(new_assets.original_value, 828.75) + self.assertEqual(new_assets.non_deductible_tax_value, 78.75) + + def test_asset_modify_value_00(self): + """Test the values of the asset and value increase 'assets' after a + modification of residual and/or salvage values. + Increase the residual value, increase the salvage value""" + self.assertEqual(self.truck.value_residual, 3000) + self.assertEqual(self.truck.salvage_value, 2500) + + self.env['asset.modify'].create({ + 'name': 'New beautiful sticker :D', + 'asset_id': self.truck.id, + 'value_residual': 4000, + 'salvage_value': 3000, + 'date': fields.Date.today() + relativedelta(months=-6, days=-1), + "account_asset_counterpart_id": self.assert_counterpart_account_id, + "account_depreciation_id": self.company_data['default_account_assets'].id, + }).modify() + self.assertEqual(self.truck.value_residual, 3000) + self.assertEqual(self.truck.salvage_value, 2500) + self.assertEqual(self.truck.children_ids.value_residual, 1000) + self.assertEqual(self.truck.children_ids.salvage_value, 500) + self.assertEqual(self.truck.account_depreciation_id.id, self.company_data['default_account_assets'].id) + + def test_asset_modify_value_01(self): + "Decrease the residual value, decrease the salvage value" + self.env['asset.modify'].create({ + 'name': "Accident :'(", + 'date': fields.Date.today() + relativedelta(months=-6, days=-1), + 'asset_id': self.truck.id, + 'value_residual': 1000, + 'salvage_value': 2000, + "account_asset_counterpart_id": self.assert_counterpart_account_id, + }).modify() + self.assertEqual(self.truck.value_residual, 1000) + self.assertEqual(self.truck.salvage_value, 2000) + self.assertEqual(self.truck.children_ids.value_residual, 0) + self.assertEqual(self.truck.children_ids.salvage_value, 0) + self.assertEqual(max(self.truck.depreciation_move_ids.filtered(lambda m: m.state == 'posted'), key=lambda m: (m.date, m.id)).amount_total, 2500) + + def test_asset_modify_value_02(self): + "Decrease the residual value, increase the salvage value; same book value" + self.env['asset.modify'].create({ + 'name': "Don't wanna depreciate all of it", + 'asset_id': self.truck.id, + 'date': fields.Date.today() + relativedelta(months=-6, days=-1), + 'value_residual': 1000, + 'salvage_value': 4500, + "account_asset_counterpart_id": self.assert_counterpart_account_id, + }).modify() + self.assertEqual(self.truck.value_residual, 1000) + self.assertEqual(self.truck.salvage_value, 4500) + self.assertEqual(self.truck.children_ids.value_residual, 0) + self.assertEqual(self.truck.children_ids.salvage_value, 0) + + def test_asset_modify_value_03(self): + "Decrease the residual value, increase the salvage value; increase of book value" + self.env['asset.modify'].create({ + 'name': "Some aliens did something to my truck", + 'asset_id': self.truck.id, + 'date': fields.Date.today() + relativedelta(months=-6, days=-1), + 'value_residual': 1000, + 'salvage_value': 6000, + "account_asset_counterpart_id": self.assert_counterpart_account_id, + }).modify() + self.assertEqual(self.truck.value_residual, 1000) + self.assertEqual(self.truck.salvage_value, 4500) + self.assertEqual(self.truck.children_ids.value_residual, 0) + self.assertEqual(self.truck.children_ids.salvage_value, 1500) + + def test_asset_modify_value_04(self): + "Increase the residual value, decrease the salvage value; increase of book value" + self.env['asset.modify'].create({ + 'name': 'GODZILA IS REAL!', + 'asset_id': self.truck.id, + 'date': fields.Date.today() + relativedelta(months=-6, days=-1), + 'value_residual': 4000, + 'salvage_value': 2000, + "account_asset_counterpart_id": self.assert_counterpart_account_id, + }).modify() + self.assertEqual(self.truck.value_residual, 3500) + self.assertEqual(self.truck.salvage_value, 2000) + self.assertEqual(self.truck.children_ids.value_residual, 500) + self.assertEqual(self.truck.children_ids.salvage_value, 0) + + def test_asset_modify_report(self): + """Test the asset value modification flows""" + # PY + - Final PY + - Final Bookvalue + # -6 0 10000 0 10000 0 750 0 750 9250 + # -5 10000 0 0 10000 750 750 0 1500 8500 + # -4 10000 0 0 10000 1500 750 0 2250 7750 + # -3 10000 0 0 10000 2250 750 0 3000 7000 + # -2 10000 0 0 10000 3000 750 0 3750 6250 + # -1 10000 0 0 10000 3750 750 0 4500 5500 + # 0 10000 0 0 10000 4500 750 0 5250 4750 <-- today + # 1 10000 0 0 10000 5250 750 0 6000 4000 + # 2 10000 0 0 10000 6000 750 0 6750 3250 + # 3 10000 0 0 10000 6750 750 0 7500 2500 + + today = fields.Date.today() + + report = self.env.ref('at_accounting.assets_report') + # TEST REPORT + # look at all period, with unposted entries + options = self._generate_options(report, today + relativedelta(years=-6, month=1, day=1), today + relativedelta(years=+4, month=12, day=31)) + lines = report._get_lines({**options, **{'unfold_all': False, 'all_entries': True}}) + self.assertListEqual([ 0.0, 10000.0, 0.0, 10000.0, 0.0, 7500.0, 0.0, 7500.0, 2500.0], + [x['no_format'] for x in lines[0]['columns'][4:]]) + + # look at all period, without unposted entries + options = self._generate_options(report, today + relativedelta(years=-6, month=1, day=1), today + relativedelta(years=+4, month=12, day=31)) + lines = report._get_lines({**options, **{'unfold_all': False, 'all_entries': False}}) + self.assertListEqual([ 0.0, 10000.0, 0.0, 10000.0, 0.0, 4500.0, 0.0, 4500.0, 5500.0], + [x['no_format'] for x in lines[0]['columns'][4:]]) + + # look only at this period + options = self._generate_options(report, today + relativedelta(years=0, month=1, day=1), today + relativedelta(years=0, month=12, day=31)) + lines = report._get_lines({**options, **{'unfold_all': False, 'all_entries': True}}) + self.assertListEqual([10000.0, 0.0, 0.0, 10000.0, 4500.0, 750.0, 0.0, 5250.0, 4750.0], + [x['no_format'] for x in lines[0]['columns'][4:]]) + + # test value increase + # PY + - Final PY + - Final Bookvalue + # -6 0 10000 0 10000 750 0 750 9250 + # -5 10000 0 0 10000 750 750 0 1500 8500 + # -4 10000 0 0 10000 1500 750 0 2250 7750 + # -3 10000 0 0 10000 2250 750 0 3000 7000 + # -2 10000 0 0 10000 3000 750 0 3750 6250 + # -1 10000 1500 0 10000 3750 950 0 4700 6800 + # 0 10000 0 0 11500 4700 950 0 5650 5850 <-- today + # 1 11500 0 0 11500 5650 950 0 6600 4900 + # 2 11500 0 0 11500 6600 950 0 7550 3950 + # 3 11500 0 0 11500 7550 950 0 8500 3000 + self.assertEqual(self.truck.value_residual, 3000) + self.assertEqual(self.truck.salvage_value, 2500) + self.env['asset.modify'].create({ + 'name': 'New beautiful sticker :D', + 'asset_id': self.truck.id, + 'date': fields.Date.today() + relativedelta(years=-1, months=-6, days=-1), + 'value_residual': 4750, + 'salvage_value': 3000, + "account_asset_counterpart_id": self.assert_counterpart_account_id, + }).modify() + + self.assertEqual(self.truck.value_residual + sum(self.truck.children_ids.mapped('value_residual')), 3800) + self.assertEqual(self.truck.salvage_value + sum(self.truck.children_ids.mapped('salvage_value')), 3000) + + # look at all period, with unposted entries + options = self._generate_options(report, today + relativedelta(years=-6, months=-6), today + relativedelta(years=+4, month=12, day=31)) + lines = report._get_lines({**options, **{'unfold_all': False, 'all_entries': True}}) + self.assertListEqual([0.0, 11500.0, 0.0, 11500.0, 0.0, 8500.0, 0.0, 8500.0, 3000.0], + [x['no_format'] for x in lines[0]['columns'][4:]]) + self.assertEqual('10 y', lines[1]['columns'][3]['name'], 'Depreciation Rate = 10%') + + # look only at this period + options = self._generate_options(report, today + relativedelta(years=0, month=1, day=1), today + relativedelta(years=0, month=12, day=31)) + lines = report._get_lines({**options, **{'unfold_all': False, 'all_entries': True}}) + self.assertListEqual([11500.0, 0.0, 0.0, 11500.0, 4700.0, 950.0, 0.0, 5650.0, 5850.0], + [x['no_format'] for x in lines[0]['columns'][4:]]) + + # test value decrease + self.env['asset.modify'].create({ + 'name': "Huge scratch on beautiful sticker :'( It is ruined", + 'date': fields.Date.today() + relativedelta(months=-6, days=-1), + 'asset_id': self.truck.children_ids.id, + 'value_residual': 0, + 'salvage_value': 500, + "account_asset_counterpart_id": self.assert_counterpart_account_id, + }).modify() + self.env['asset.modify'].create({ + 'name': "Huge scratch on beautiful sticker :'( It went through...", + 'date': fields.Date.today() + relativedelta(months=-6, days=-1), + 'asset_id': self.truck.id, + 'value_residual': 1000, + 'salvage_value': 2500, + "account_asset_counterpart_id": self.assert_counterpart_account_id, + }).modify() + self.assertEqual(self.truck.value_residual + sum(self.truck.children_ids.mapped('value_residual')), 1000) + self.assertEqual(self.truck.salvage_value + sum(self.truck.children_ids.mapped('salvage_value')), 3000) + + # look at all period, with unposted entries + options = self._generate_options(report, today + relativedelta(years=-6, month=1, day=1), today + relativedelta(years=+4, month=12, day=31)) + lines = report._get_lines({**options, **{'unfold_all': False, 'all_entries': True}}) + self.assertListEqual([0.0, 11500.0, 0.0, 11500.0, 0.0, 8500.0, 0.0, 8500.0, 3000.0], + [x['no_format'] for x in lines[0]['columns'][4:]]) + + # look only at previous period + options = self._generate_options(report, today + relativedelta(years=-1, month=1, day=1), today + relativedelta(years=-1, month=12, day=31)) + lines = report._get_lines({**options, **{'unfold_all': False, 'all_entries': True}}) + self.assertListEqual([10000.0, 1500.0, 0.0, 11500.0, 3750.0, 3750.0, 0.0, 7500.0, 4000.0], + [x['no_format'] for x in lines[0]['columns'][4:]]) + + def test_asset_pause_resume(self): + """Test that depreciation remains the same after a pause and resume at a later date""" + today = fields.Date.today() + self.assertEqual(len(self.truck.depreciation_move_ids.filtered(lambda e: e.state == 'draft')), 4) + self.env['asset.modify'].create({ + 'date': fields.Date.today() + relativedelta(days=-1), + 'asset_id': self.truck.id, + }).pause() + self.assertEqual(len(self.truck.depreciation_move_ids.filtered(lambda e: e.state == 'draft')), 0) + with freeze_time(today) as frozen_time: + frozen_time.move_to(today + relativedelta(years=1)) + self.env['asset.modify'].with_context(resume_after_pause=True).create({ + 'asset_id': self.truck.id, + }).modify() + self.assertEqual(len(self.truck.depreciation_move_ids.filtered(lambda e: e.state == 'posted')), 7) + self.assertEqual( + self.truck.depreciation_move_ids.filtered(lambda e: e.state == 'draft').mapped('amount_total'), + [375.0, 750.0, 750.0, 750.0]) + + def test_asset_modify_sell_profit(self): + """Test that a credit is realised in the gain account when selling an asset for a sum greater than book value""" + closing_invoice = self.env['account.move'].create({ + 'move_type': 'out_invoice', + 'invoice_line_ids': [(0, 0, {'price_unit': self.truck.book_value + 100})] + }) + self.env['asset.modify'].create({ + 'asset_id': self.truck.id, + 'invoice_line_ids': closing_invoice.invoice_line_ids, + 'date': fields.Date.today() + relativedelta(months=-6, days=-1), + 'modify_action': 'sell', + }).sell_dispose() + + closing_move = self.truck.depreciation_move_ids.filtered(lambda l: l.state == 'draft') + self.assertRecordValues(closing_move.line_ids, [{ + 'ref': 'truck: Sale', + 'debit': 0, + 'credit': 10000, + 'account_id': self.truck.account_asset_id.id, + }, { + 'ref': 'truck: Sale', + 'debit': 4500, + 'credit': 0, + 'account_id': self.truck.account_depreciation_id.id, + }, { + 'ref': 'truck: Sale', + 'debit': 5600, + 'credit': 0, + 'account_id': closing_invoice.invoice_line_ids.account_id.id, + }, { + 'ref': 'truck: Sale', + 'debit': 0, + 'credit': 100, + 'account_id': self.env.company.gain_account_id.id, + }]) + + def test_asset_modify_sell_loss(self): + """Test that a debit is realised in the loss account when selling an asset for a sum less than book value""" + closing_invoice = self.env['account.move'].create({ + 'move_type': 'out_invoice', + 'invoice_line_ids': [(0, 0, {'price_unit': self.truck.book_value - 100})] + }) + self.env['asset.modify'].create({ + 'asset_id': self.truck.id, + 'invoice_line_ids': closing_invoice.invoice_line_ids, + 'date': fields.Date.today() + relativedelta(months=-6, days=-1), + 'modify_action': 'sell', + }).sell_dispose() + closing_move = self.truck.depreciation_move_ids.filtered(lambda l: l.state == 'draft') + + self.assertRecordValues(closing_move.line_ids, [{ + 'ref': 'truck: Sale', + 'debit': 0, + 'credit': 10000, + 'account_id': self.truck.account_asset_id.id, + }, { + 'ref': 'truck: Sale', + 'debit': 4500, + 'credit': 0, + 'account_id': self.truck.account_depreciation_id.id, + }, { + 'ref': 'truck: Sale', + 'debit': 5400, + 'credit': 0, + 'account_id': closing_invoice.invoice_line_ids.account_id.id, + }, { + 'ref': 'truck: Sale', + 'debit': 100, + 'credit': 0, + 'account_id': self.env.company.loss_account_id.id, + }]) + + def test_asset_sale_same_account_as_invoice(self): + """Test the sale of an asset with an invoice that has the same account as the Depreciation Account""" + closing_invoice = self.env['account.move'].create({ + 'move_type': 'out_invoice', + 'invoice_line_ids': [ + Command.create({ + 'account_id': self.truck.account_depreciation_id.id, + 'price_unit': self.truck.book_value - 100 + }) + ] + }) + self.env['asset.modify'].create({ + 'asset_id': self.truck.id, + 'invoice_line_ids': closing_invoice.invoice_line_ids, + 'date': fields.Date.today() + relativedelta(months=-6, days=-1), + 'modify_action': 'sell', + }).sell_dispose() + closing_move = self.truck.depreciation_move_ids.filtered(lambda l: l.state == 'draft') + self.assertRecordValues(closing_move.line_ids, [{ + 'ref': 'truck: Sale', + 'debit': 0, + 'credit': 10000, + 'account_id': self.truck.account_asset_id.id, + }, { + 'ref': 'truck: Sale', + 'debit': 4500, + 'credit': 0, + 'account_id': self.truck.account_depreciation_id.id, + }, { + 'ref': 'truck: Sale', + 'debit': 5400, + 'credit': 0, + 'account_id': closing_invoice.invoice_line_ids.account_id.id, + }, { + 'ref': 'truck: Sale', + 'debit': 100, + 'credit': 0, + 'account_id': self.env.company.loss_account_id.id, + }]) + + self.assertEqual(closing_move.depreciation_value, 3000, "Should be the remaining amount before the sale") + + def test_asset_modify_dispose(self): + """Test the loss of the remaining book_value when an asset is disposed using the wizard""" + self.env['asset.modify'].create({ + 'asset_id': self.truck.id, + 'date': fields.Date.today() + relativedelta(months=-6, days=-1), + 'modify_action': 'dispose', + }).sell_dispose() + closing_move = self.truck.depreciation_move_ids.filtered(lambda l: l.state == 'draft') + self.assertRecordValues(closing_move.line_ids, [{ + 'ref': 'truck: Disposal', + 'debit': 0, + 'credit': 10000, + 'account_id': self.truck.account_asset_id.id, + }, { + 'ref': 'truck: Disposal', + 'debit': 4500, + 'credit': 0, + 'account_id': self.truck.account_depreciation_id.id, + }, { + 'ref': 'truck: Disposal', + 'debit': 5500, + 'credit': 0, + 'account_id': self.env.company.loss_account_id.id, + }]) + + def test_asset_reverse_depreciation(self): + """Test the reversal of a depreciation move""" + + self.assertEqual(sum(self.truck.depreciation_move_ids.filtered(lambda m: m.state == 'posted').mapped('depreciation_value')), 4500) + self.assertEqual(sum(self.truck.depreciation_move_ids.filtered(lambda m: m.state == 'draft').mapped('depreciation_value')), 3000) + self.assertEqual(max(self.truck.depreciation_move_ids.filtered(lambda m: m.state == 'posted'), key=lambda m: m.date).asset_remaining_value, 3000) + + report = self.env.ref('at_accounting.assets_report') + today = fields.Date.today() + + move_to_reverse = self.truck.depreciation_move_ids.filtered(lambda m: m.state == 'posted').sorted(lambda m: m.date)[-1] + reversed_move = move_to_reverse._reverse_moves() + + # Check that the depreciation has been reported on the next move + min_date_draft = min(self.truck.depreciation_move_ids.filtered(lambda m: m.state == 'draft' and m.date > reversed_move.date), key=lambda m: m.date) + self.assertEqual(move_to_reverse.asset_remaining_value - min_date_draft.depreciation_value - reversed_move.depreciation_value, min_date_draft.asset_remaining_value) + self.assertEqual(move_to_reverse.asset_depreciated_value + min_date_draft.depreciation_value + reversed_move.depreciation_value, min_date_draft.asset_depreciated_value) + + # The amount is still there, it only has been reversed. But it has been added on the next draft move to complete the depreciation table + self.assertEqual(sum(self.truck.depreciation_move_ids.filtered(lambda m: m.state == 'posted').mapped('depreciation_value')), 4500) + self.assertEqual(sum(self.truck.depreciation_move_ids.filtered(lambda m: m.state == 'draft').mapped('depreciation_value')), 3000) + + # Check that the table shows fully depreciated at the end + self.assertEqual(max(self.truck.depreciation_move_ids, key=lambda m: m.date).asset_remaining_value, 0) + self.assertEqual(max(self.truck.depreciation_move_ids, key=lambda m: m.date).asset_depreciated_value, 7500) + + reversed_move.action_post() + + options = self._generate_options(report, today + relativedelta(years=0, month=7, day=1), today + relativedelta(years=0, month=7, day=31)) + lines = report._get_lines({**options, 'unfold_all': False, 'all_entries': True}) + # We take the reversal entry into account + self.assertListEqual([10000.0, 0.0, 0.0, 10000.0, 4500.0, -750.0, 0.0, 3750.0, 6250.0], + [x['no_format'] for x in lines[0]['columns'][4:]]) + + options = self._generate_options(report, today + relativedelta(years=0, month=1, day=1), today + relativedelta(years=0, month=12, day=31)) + lines = report._get_lines({**options, 'unfold_all': False, 'all_entries': True}) + # With the report on the next entry, we get a normal depreciation amount for the year + self.assertListEqual([10000.0, 0.0, 0.0, 10000.0, 4500.0, 750.0, 0.0, 5250.0, 4750.0], + [x['no_format'] for x in lines[0]['columns'][4:]]) + + def test_ref_asset_depreciation(self): + """Test that the reference used in depreciation moves is correct""" + + for ref in self.truck.depreciation_move_ids.mapped('ref'): + self.assertEqual(ref, 'truck: Depreciation') + + def test_credit_note_out_refund(self): + """ + Test the behaviour of the asset creation when a credit note is created. + The asset created from the credit note should be the same as the one created from the invoice + with a negative value. + """ + depreciation_account = self.company_data['default_account_assets'].copy() + revenue_model = self.env['account.asset'].create({ + 'account_depreciation_id': depreciation_account.id, + 'account_depreciation_expense_id': self.company_data['default_account_revenue'].id, + 'journal_id': self.company_data['default_journal_misc'].id, + 'name': 'Hardware - 5 Years', + 'method_number': 5, + 'method_period': '12', + 'state': 'model', + }) + + depreciation_account.write({'create_asset': 'draft', 'asset_model_ids': revenue_model}) + + invoice = self.env['account.move'].create({ + 'invoice_date': '2019-07-01', + 'move_type': 'in_invoice', + 'partner_id': self.partner_a.id, + 'invoice_line_ids': [(0, 0, { + 'name': 'Hardware', + 'account_id': depreciation_account.id, + 'price_unit': 5000, + 'quantity': 1, + 'tax_ids': False, + })], + }) + + invoice.action_post() + self.assertTrue(invoice.asset_ids) + + credit_note = invoice._reverse_moves([{'invoice_date': fields.Date.today()}]) + credit_note.action_post() + + invoice_asset = invoice.asset_ids + credit_note_asset = credit_note.asset_ids + + # check if invoice_asset still exists after validate the credit note + self.assertTrue(invoice_asset) + self.assertTrue(credit_note_asset) + + (invoice_asset + credit_note_asset).validate() + + self.assertRecordValues(credit_note_asset, [ + { + 'acquisition_date': invoice_asset.acquisition_date, + 'book_value': -invoice_asset.book_value, + 'value_residual': -invoice_asset.value_residual, + } + ]) + + for invoice_asset_move, credit_note_asset_move in zip(invoice_asset.depreciation_move_ids.sorted('date'), credit_note_asset.depreciation_move_ids.sorted('date')): + self.assertRecordValues(credit_note_asset_move, [ + { + 'date': invoice_asset_move.date, + 'state': invoice_asset_move.state, + 'depreciation_value': -invoice_asset_move.depreciation_value, + } + ]) + + def test_asset_multiple_assets_from_one_move_line_00(self): + """ Test the creation of a as many assets as the value of + the quantity property of a move line. """ + + account = self.env['account.account'].create({ + "name": "test account", + "code": "TEST", + "account_type": 'asset_non_current', + "create_asset": "draft", + "multiple_assets_per_line": True, + }) + move = self.env['account.move'].create({ + "partner_id": self.env['res.partner'].create({'name': 'Johny'}).id, + "ref": "line1", + "move_type": "in_invoice", + "invoice_date": "2020-12-31", + "invoice_line_ids": [ + (0, 0, { + "account_id": account.id, + "price_unit": 400.0, + "name": "stuff", + "quantity": 2, + "product_uom_id": self.env.ref('uom.product_uom_unit').id, + "tax_ids": [], + }), + ] + }) + move.action_post() + assets = move.asset_ids + assets = sorted(assets, key=lambda i: i['original_value'], reverse=True) + self.assertEqual(len(assets), 2, '3 assets should have been created') + self.assertEqual(assets[0].original_value, 400.0) + self.assertEqual(assets[1].original_value, 400.0) + + def test_asset_multiple_assets_from_one_move_line_01(self): + """ Test the creation of a as many assets as the value of + the quantity property of a move line. """ + + account = self.env['account.account'].create({ + "name": "test account", + "code": "TEST", + "account_type": 'asset_non_current', + "create_asset": "draft", + "multiple_assets_per_line": True, + }) + move = self.env['account.move'].create({ + "partner_id": self.env['res.partner'].create({'name': 'Johny'}).id, + "ref": "line1", + "move_type": "in_invoice", + "invoice_date": "2020-12-31", + "invoice_line_ids": [ + (0, 0, { + "account_id": account.id, + "name": "stuff", + "quantity": 3.0, + "price_unit": 1000.0, + "product_uom_id": self.env.ref('uom.product_uom_categ_unit').id, + }), + (0, 0, { + 'account_id': self.company_data['default_account_assets'].id, + "name": "stuff", + 'quantity': 1.0, + 'price_unit': -500.0, + }), + ] + }) + move.action_post() + self.assertEqual(sum(asset.original_value for asset in move.asset_ids), move.line_ids[0].debit) + + def test_asset_credit_note(self): + """Test the generated entries created from an in_refund invoice with asset""" + asset_model = self.env['account.asset'].create({ + 'account_depreciation_id': self.company_data['default_account_assets'].id, + 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, + 'account_asset_id': self.company_data['default_account_assets'].id, + 'journal_id': self.company_data['default_journal_purchase'].id, + 'name': 'Small car - 3 Years', + 'method_number': 3, + 'method_period': '12', + 'state': 'model', + }) + + self.company_data['default_account_assets'].create_asset = "validate" + self.company_data['default_account_assets'].asset_model_ids = asset_model + + invoice = self.env['account.move'].create({ + 'move_type': 'in_refund', + 'invoice_date': '2020-01-01', + 'date': '2020-01-01', + 'partner_id': self.partner_a.id, + 'invoice_line_ids': [(0, 0, { + 'name': 'Very little red car', + 'account_id': self.company_data['default_account_assets'].id, + 'price_unit': 450, + 'quantity': 1, + })], + }) + invoice.action_post() + depreciation_lines = self.env['account.move.line'].search([ + ('account_id', '=', asset_model.account_depreciation_id.id), + ('move_id.asset_id', '=', invoice.asset_ids.id), + ('debit', '=', 150), + ]) + self.assertEqual( + len(depreciation_lines), 3, + 'Three entries with a debit of 150 must be created on the Deferred Expense Account' + ) + + def test_asset_partial_credit_note(self): + """Test partial credit note on an in invoice that has generated draft assets. + + Test case: + - Create in invoice with the following lines: + + Product | Unit Price | Quantity | Multiple assets + --------------------------------------------------------- + Product B | 200 | 4 | TRUE + Product A | 100 | 7 | FALSE + Product A | 100 | 5 | TRUE + Product A | 150 | 6 | TRUE + Product A | 100 | 7 | FALSE + + - Add a credit note with the following lines: + + Product | Unit Price | Quantity + --------------------------------------- + Product A | 100 | 1 + Product A | 150 | 2 + Product A | 100 | 7 + """ + asset_model = self.env['account.asset'].create({ + 'account_depreciation_id': self.company_data['default_account_assets'].id, + 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, + 'journal_id': self.company_data['default_journal_sale'].id, + 'name': 'Maintenance Contract - 3 Years', + 'method_number': 3, + 'method_period': '12', + 'prorata_computation_type': 'none', + 'state': 'model', + }) + self.company_data['default_account_assets'].create_asset = 'draft' + self.company_data['default_account_assets'].asset_model_ids = asset_model + account_assets_multiple = self.company_data['default_account_assets'].copy() + account_assets_multiple.multiple_assets_per_line = True + + product_a = self.env['product.product'].create({ + 'name': 'Product A', + 'default_code': 'PA', + 'lst_price': 100.0, + 'standard_price': 100.0, + }) + product_b = self.env['product.product'].create({ + 'name': 'Product B', + 'default_code': 'PB', + 'lst_price': 200.0, + 'standard_price': 200.0, + }) + invoice = self.env['account.move'].create({ + 'move_type': 'in_invoice', + 'invoice_date': '2020-01-01', + 'partner_id': self.partner_a.id, + 'invoice_line_ids': [ + (0, 0, { + 'product_id': product_b.id, + 'name': 'Product B', + 'account_id': account_assets_multiple.id, + 'price_unit': 200.0, + 'quantity': 4, + }), + (0, 0, { + 'product_id': product_a.id, + 'name': 'Product A', + 'account_id': self.company_data['default_account_assets'].id, + 'price_unit': 100.0, + 'quantity': 7, + }), + (0, 0, { + 'product_id': product_a.id, + 'name': 'Product A', + 'account_id': account_assets_multiple.id, + 'price_unit': 100.0, + 'quantity': 5, + }), + (0, 0, { + 'product_id': product_a.id, + 'name': 'Product A', + 'account_id': account_assets_multiple.id, + 'price_unit': 150.0, + 'quantity': 6, + }), + (0, 0, { + 'product_id': product_a.id, + 'name': 'Product A', + 'account_id': self.company_data['default_account_assets'].id, + 'price_unit': 100.0, + 'quantity': 7, + }), + ], + }) + invoice.action_post() + product_a_100_lines = invoice.line_ids.filtered(lambda l: l.product_id == product_a and l.price_unit == 100.0) + product_a_150_lines = invoice.line_ids.filtered(lambda l: l.product_id == product_a and l.price_unit == 150.0) + product_b_lines = invoice.line_ids.filtered(lambda l: l.product_id == product_b) + self.assertEqual(len(invoice.line_ids.mapped(lambda l: l.asset_ids)), 17) + self.assertEqual(len(product_b_lines.asset_ids), 4) + self.assertEqual(len(product_a_100_lines.asset_ids), 7) + self.assertEqual(len(product_a_150_lines.asset_ids), 6) + credit_note = invoice._reverse_moves() + with Form(credit_note) as move_form: + move_form.invoice_date = move_form.date + move_form.invoice_line_ids.remove(0) + move_form.invoice_line_ids.remove(0) + with move_form.invoice_line_ids.edit(0) as line_form: + line_form.quantity = 1 + with move_form.invoice_line_ids.edit(1) as line_form: + line_form.quantity = 2 + credit_note.action_post() + self.assertEqual(len(invoice.line_ids.mapped(lambda l: l.asset_ids)), 17) + self.assertEqual(len(product_b_lines.asset_ids), 4) + self.assertEqual(len(product_a_100_lines.asset_ids), 7) + self.assertEqual(len(product_a_150_lines.asset_ids), 6) + + def test_asset_with_non_deductible_tax(self): + """Test that the assets' original_value and non_deductible_tax_value are correctly computed + from a move line with a non-deductible tax.""" + + asset_account = self.company_data['default_account_assets'] + non_deductible_tax = self.env['account.tax'].create({ + 'name': 'Non-deductible Tax', + 'amount': 21, + 'amount_type': 'percent', + 'type_tax_use': 'purchase', + 'invoice_repartition_line_ids': [ + Command.create({'repartition_type': 'base'}), + Command.create({ + 'factor_percent': 50, + 'repartition_type': 'tax', + 'use_in_tax_closing': False + }), + Command.create({ + 'factor_percent': 50, + 'repartition_type': 'tax', + 'use_in_tax_closing': True + }), + ], + 'refund_repartition_line_ids': [ + Command.create({'repartition_type': 'base'}), + Command.create({ + 'factor_percent': 50, + 'repartition_type': 'tax', + 'use_in_tax_closing': False + }), + Command.create({ + 'factor_percent': 50, + 'repartition_type': 'tax', + 'use_in_tax_closing': True + }), + ], + }) + asset_account.tax_ids = non_deductible_tax + + # 1. Automatic creation + asset_account.create_asset = 'draft' + asset_account.asset_model_ids = self.account_asset_model_fixedassets + asset_account.multiple_assets_per_line = True + + vendor_bill_auto = self.env['account.move'].create({ + 'move_type': 'in_invoice', + 'invoice_date': '2020-01-01', + 'partner_id': self.partner_a.id, + 'invoice_line_ids': [Command.create({ + 'account_id': asset_account.id, + 'name': 'Asus Laptop', + 'price_unit': 1000.0, + 'quantity': 2, + 'tax_ids': [Command.set(non_deductible_tax.ids)], + })], + }) + vendor_bill_auto.action_post() + + new_assets_auto = vendor_bill_auto.asset_ids + self.assertEqual(len(new_assets_auto), 2) + self.assertEqual(new_assets_auto.mapped('original_value'), [1105.0, 1105.0]) + self.assertEqual(new_assets_auto.mapped('non_deductible_tax_value'), [105.0, 105.0]) + + # 2. Manual creation + asset_account.create_asset = 'no' + asset_account.asset_model_ids = None + asset_account.multiple_assets_per_line = False + + vendor_bill_manu = self.env['account.move'].create({ + 'move_type': 'in_invoice', + 'invoice_date': '2020-01-01', + 'partner_id': self.partner_a.id, + 'invoice_line_ids': [ + Command.create({ + 'account_id': asset_account.id, + 'name': 'Asus Laptop', + 'price_unit': 1000.0, + 'quantity': 2, + 'tax_ids': [Command.set(non_deductible_tax.ids)] + }), + Command.create({ + 'account_id': asset_account.id, + 'name': 'Lenovo Laptop', + 'price_unit': 500.0, + 'quantity': 3, + 'tax_ids': [Command.set(non_deductible_tax.ids)] + }), + ], + }) + vendor_bill_manu.action_post() + + # TOFIX: somewhere above this the field account.asset.asset_type is made + # dirty, but this field has to be flushed in a specific environment. + # This is because the field 'asset_type' is stored, computed and + # context-dependent, which explains why its value must be retrieved + # from the right environment. + self.env.flush_all() + + move_line_ids = vendor_bill_manu.mapped('line_ids').filtered(lambda x: 'Laptop' in x.name) + asset_form = Form(self.env['account.asset'].with_context( + default_original_move_line_ids=move_line_ids.ids, + )) + asset_form.original_move_line_ids = move_line_ids + asset_form.account_depreciation_expense_id = self.company_data['default_account_expense'] + + new_assets_manu = asset_form.save() + self.assertEqual(len(new_assets_manu), 1) + self.assertEqual(new_assets_manu.original_value, 3867.5) + self.assertEqual(new_assets_manu.non_deductible_tax_value, 367.5) + + def test_asset_degressive_01(self): + """ Check the computation of an asset with degressive method, + start at middle of the year + """ + asset = self.env['account.asset'].create({ + 'account_asset_id': self.company_data['default_account_assets'].id, + 'account_depreciation_id': self.company_data['default_account_assets'].id, + 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, + 'journal_id': self.company_data['default_journal_misc'].id, + 'name': 'Degressive', + 'acquisition_date': '2021-07-01', + 'prorata_computation_type': 'constant_periods', + 'original_value': 10000, + 'method_number': 5, + 'method_period': '12', + 'method': 'degressive', + 'method_progress_factor': 0.5, + }) + + asset.validate() + + self.assertEqual(asset.method_number + 1, len(asset.depreciation_move_ids)) + + self.assertRecordValues(asset.depreciation_move_ids.sorted(lambda l: (l.date, l.id)), [{ + 'amount_total': 2500, + 'asset_remaining_value': 7500, + }, { + 'amount_total': 3750, + 'asset_remaining_value': 3750, + }, { + 'amount_total': 1875, + 'asset_remaining_value': 1875, + }, { + 'amount_total': 937.5, + 'asset_remaining_value': 937.5, + }, { + 'amount_total': 625.00, + 'asset_remaining_value': 312.50, + }, { + 'amount_total': 312.50, + 'asset_remaining_value': 0, + }]) + + def test_asset_degressive_02(self): + """ Check the computation of an asset with degressive method, + start at beginning of the year. + """ + asset = self.env['account.asset'].create({ + 'account_asset_id': self.company_data['default_account_assets'].id, + 'account_depreciation_id': self.company_data['default_account_assets'].id, + 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, + 'journal_id': self.company_data['default_journal_misc'].id, + 'name': 'Degressive', + 'acquisition_date': '2021-01-01', + 'original_value': 10000, + 'method_number': 5, + 'method_period': '12', + 'method': 'degressive', + 'method_progress_factor': 0.5, + }) + + asset.validate() + + self.assertEqual(asset.method_number, len(asset.depreciation_move_ids)) + + self.assertRecordValues(asset.depreciation_move_ids.sorted(lambda l: (l.date, l.id)), [{ + 'amount_total': 5000, + 'asset_remaining_value': 5000, + }, { + 'amount_total': 2500, + 'asset_remaining_value': 2500, + }, { + 'amount_total': 1250, + 'asset_remaining_value': 1250, + }, { + 'amount_total': 625, + 'asset_remaining_value': 625, + }, { + 'amount_total': 625, + 'asset_remaining_value': 0, + }]) + + def test_asset_negative_01(self): + """ Check the computation of an asset with negative value. """ + asset = self.env['account.asset'].create({ + 'account_asset_id': self.company_data['default_account_assets'].id, + 'account_depreciation_id': self.company_data['default_account_assets'].id, + 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, + 'journal_id': self.company_data['default_journal_misc'].id, + 'name': 'Degressive Linear', + 'acquisition_date': '2021-07-01', + 'original_value': -10000, + 'method_number': 5, + 'method_period': '12', + 'method': 'linear', + }) + asset.prorata_computation_type = 'constant_periods' + + asset.validate() + + self.assertRecordValues(asset.depreciation_move_ids.sorted(lambda l: (l.date, l.id)), [{ + 'amount_total': 1000, + 'asset_remaining_value': -9000, + }, { + 'amount_total': 2000, + 'asset_remaining_value': -7000, + }, { + 'amount_total': 2000, + 'asset_remaining_value': -5000, + }, { + 'amount_total': 2000, + 'asset_remaining_value': -3000, + }, { + 'amount_total': 2000, + 'asset_remaining_value': -1000, + }, { + 'amount_total': 1000, + 'asset_remaining_value': 0, + }]) + + def test_asset_daily_computation_01(self): + """ Check the computation of an asset with daily_computation. """ + asset = self.env['account.asset'].create({ + 'account_asset_id': self.company_data['default_account_assets'].id, + 'account_depreciation_id': self.company_data['default_account_assets'].id, + 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, + 'journal_id': self.company_data['default_journal_misc'].id, + 'name': 'Degressive Linear', + 'acquisition_date': '2021-07-01', + 'prorata_computation_type': 'daily_computation', + 'original_value': 10000, + 'method_number': 5, + 'method_period': '12', + 'method': 'linear', + }) + + asset.validate() + + self.assertRecordValues(asset.depreciation_move_ids.sorted(lambda l: (l.date, l.id)), [{ + 'amount_total': 1007.67, + 'asset_remaining_value': 8992.33, + }, { + 'amount_total': 1998.90, + 'asset_remaining_value': 6993.43, + }, { + 'amount_total': 1998.91, + 'asset_remaining_value': 4994.52, + }, { + 'amount_total': 2004.38, + 'asset_remaining_value': 2990.14, + }, { + 'amount_total': 1998.90, + 'asset_remaining_value': 991.24, + }, { + 'amount_total': 991.24, + 'asset_remaining_value': 0, + }]) + + def test_decrement_book_value_with_negative_asset(self): + """ + Test the computation of book value and remaining value + when posting a depreciation move related with a negative asset + """ + depreciation_account = self.company_data['default_account_assets'].copy() + asset_model = self.env['account.asset'].create({ + 'name': 'test', + 'state': 'model', + 'active': True, + 'method': 'linear', + 'method_number': 5, + 'method_period': '1', + 'prorata_computation_type': 'constant_periods', + 'account_asset_id': self.company_data['default_account_assets'].id, + 'account_depreciation_id': depreciation_account.id, + 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, + 'journal_id': self.company_data['default_journal_purchase'].id, + }) + + depreciation_account.can_create_asset = True + depreciation_account.create_asset = 'draft' + depreciation_account.asset_model_ids = asset_model + + refund = self.env['account.move'].create({ + 'move_type': 'in_refund', + 'partner_id': self.partner_a.id, + 'invoice_date': '2021-06-01', + 'invoice_line_ids': [Command.create({'name': 'refund', 'account_id': depreciation_account.id, 'price_unit': 500, 'tax_ids': False})], + }) + refund.action_post() + + self.assertTrue(refund.asset_ids) + + asset = refund.asset_ids + + self.assertEqual(asset.book_value, -refund.amount_total) + self.assertEqual(asset.value_residual, -refund.amount_total) + + asset.validate() + + self.assertEqual(len(asset.depreciation_move_ids.filtered(lambda m: m.state == 'posted')), 1) + self.assertEqual(asset.book_value, -400.0) + self.assertEqual(asset.value_residual, -400.0) + + def test_depreciation_schedule_report_with_negative_asset(self): + """ + Test the computation of depreciation schedule with negative asset + """ + asset = self.env['account.asset'].create({ + 'name': 'test', + 'original_value': -500, + 'method': 'linear', + 'method_number': 5, + 'method_period': '1', + 'acquisition_date': fields.Date.today() + relativedelta(months=-1), + 'prorata_computation_type': 'none', + 'account_asset_id': self.company_data['default_account_assets'].id, + 'account_depreciation_id': self.company_data['default_account_assets'].id, + 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, + 'journal_id': self.company_data['default_journal_misc'].id, + }) + + asset.validate() + + report = self.env.ref('at_accounting.assets_report') + + options = self._generate_options(report, fields.Date.today() + relativedelta(months=-7, day=1), fields.Date.today() + relativedelta(months=-6, day=31)) + + expected_values_open_asset = [ + ("test", 0, 0, 500.0, -500.0, 0, 0, 100.0, -100.0, -400.0), + ] + + self.assertLinesValues(report._get_lines(options)[2:3], [0, 5, 6, 7, 8, 9, 10, 11, 12, 13], expected_values_open_asset, options) + + expense_account_copy = self.company_data['default_account_expense'].copy() + + disposal_action_view = self.env['asset.modify'].create({ + 'asset_id': asset.id, + 'modify_action': 'dispose', + 'loss_account_id': expense_account_copy.id, + 'date': fields.Date.today() + }).sell_dispose() + + self.env['account.move'].browse(disposal_action_view['res_id']).action_post() + + expected_values_closed_asset = [ + ("test", 0, 500.0, 500.0, 0, 0, 500.0, 500.0, 0, 0), + ] + options = self._generate_options(report, fields.Date.today() + relativedelta(months=-7, day=1), fields.Date.today()) + self.assertLinesValues(report._get_lines(options)[2:3], [0, 5, 6, 7, 8, 9, 10, 11, 12, 13], expected_values_closed_asset, options) + + def test_depreciation_schedule_hierarchy(self): + # Remove previously existing assets. + assets = self.env['account.asset'].search([ + ('company_id', '=', self.env.company.id), + ('state', '!=', 'model'), + ]) + assets.state = 'draft' + assets.mapped('depreciation_move_ids').state = 'draft' + assets.unlink() + + # Create the account groups. + self.env['account.group'].create([ + {'name': 'Group 1', 'code_prefix_start': '1', 'code_prefix_end': '1'}, + {'name': 'Group 11', 'code_prefix_start': '11', 'code_prefix_end': '11'}, + {'name': 'Group 12', 'code_prefix_start': '12', 'code_prefix_end': '12'}, + ]) + + # Create the accounts. + account_a, account_a1, account_b, account_c, account_d, account_e = self.env['account.account'].create([ + {'code': '1100', 'name': 'Account A', 'account_type': 'asset_non_current'}, + {'code': '1110', 'name': 'Account A1', 'account_type': 'asset_non_current'}, + {'code': '1200', 'name': 'Account B', 'account_type': 'asset_non_current'}, + {'code': '1300', 'name': 'Account C', 'account_type': 'asset_non_current'}, + {'code': '1400', 'name': 'Account D', 'account_type': 'asset_non_current'}, + {'code': '9999', 'name': 'Account E', 'account_type': 'asset_non_current'}, + ]) + + # Create and validate the assets, and post the depreciation entries. + self.env['account.asset'].create([ + { + 'account_asset_id': account_id, + 'account_depreciation_id': account_id, + 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, + 'journal_id': self.company_data['default_journal_misc'].id, + 'name': name, + 'acquisition_date': fields.Date.to_date('2020-07-01'), + 'original_value': original_value, + 'method': 'linear', + 'prorata_computation_type': 'none', + } + for account_id, name, original_value in [ + (account_a.id, 'ZenBook', 1250), + (account_a.id, 'ThinkBook', 1500), + (account_a1.id, 'XPS', 1750), + (account_b.id, 'MacBook', 2000), + (account_c.id, 'Aspire', 1600), + (account_d.id, 'Playstation', 550), + (account_e.id, 'Xbox', 500), + ] + ]).validate() + + # Configure the depreciation schedule report. + report = self.env.ref('at_accounting.assets_report') + options = self._generate_options(report, '2022-01-01', '2022-12-31') + options['hierarchy'] = True + self.env.company.totals_below_sections = True + + # Generate and compare actual VS expected values. + lines = [ + { + 'name': line['name'], + 'level': line['level'], + 'book_value': line['columns'][-1]['name'] + } + for line in (report._get_lines(options)) + ] + + expected_values = [ + # pylint: disable=C0326 + {'name': '1 Group 1', 'level': 1, 'book_value': '$\xa06,920.00'}, + {'name': '11 Group 11', 'level': 2, 'book_value': '$\xa03,600.00'}, + {'name': '1100 Account A', 'level': 3, 'book_value': '$\xa02,200.00'}, + {'name': 'ZenBook', 'level': 4, 'book_value': '$\xa01,000.00'}, + {'name': 'ThinkBook', 'level': 4, 'book_value': '$\xa01,200.00'}, + {'name': 'Total 1100 Account A', 'level': 3, 'book_value': '$\xa02,200.00'}, + {'name': '1110 Account A1', 'level': 3, 'book_value': '$\xa01,400.00'}, + {'name': 'XPS', 'level': 4, 'book_value': '$\xa01,400.00'}, + {'name': 'Total 1110 Account A1', 'level': 3, 'book_value': '$\xa01,400.00'}, + {'name': 'Total 11 Group 11', 'level': 2, 'book_value': '$\xa03,600.00'}, + {'name': '12 Group 12', 'level': 2, 'book_value': '$\xa01,600.00'}, + {'name': '1200 Account B', 'level': 3, 'book_value': '$\xa01,600.00'}, + {'name': 'MacBook', 'level': 4, 'book_value': '$\xa01,600.00'}, + {'name': 'Total 1200 Account B', 'level': 3, 'book_value': '$\xa01,600.00'}, + {'name': 'Total 12 Group 12', 'level': 2, 'book_value': '$\xa01,600.00'}, + {'name': '1300 Account C', 'level': 2, 'book_value': '$\xa01,280.00'}, + {'name': 'Aspire', 'level': 3, 'book_value': '$\xa01,280.00'}, + {'name': 'Total 1300 Account C', 'level': 2, 'book_value': '$\xa01,280.00'}, + {'name': '1400 Account D', 'level': 2, 'book_value': '$\xa0440.00'}, + {'name': 'Playstation', 'level': 3, 'book_value': '$\xa0440.00'}, + {'name': 'Total 1400 Account D', 'level': 2, 'book_value': '$\xa0440.00'}, + {'name': 'Total 1 Group 1', 'level': 1, 'book_value': '$\xa06,920.00'}, + {'name': '(No Group)', 'level': 1, 'book_value': '$\xa0400.00'}, + {'name': '9999 Account E', 'level': 2, 'book_value': '$\xa0400.00'}, + {'name': 'Xbox', 'level': 3, 'book_value': '$\xa0400.00'}, + {'name': 'Total 9999 Account E', 'level': 2, 'book_value': '$\xa0400.00'}, + {'name': 'Total (No Group)', 'level': 1, 'book_value': '$\xa0400.00'}, + {'name': 'Total', 'level': 1, 'book_value': '$\xa07,320.00'}, + ] + + self.assertEqual(len(lines), len(expected_values)) + self.assertEqual(lines, expected_values) + + def test_depreciation_schedule_disposal_move_unposted(self): + """ + Test the computation of values when disposing an asset, and the difference if the disposal move is posted + """ + asset = self.env['account.asset'].create({ + 'name': 'test asset', + 'method': 'linear', + 'original_value': 1000, + 'method_number': 5, + 'method_period': '12', + 'acquisition_date': fields.Date.today() + relativedelta(years=-2, month=1, day=1), + 'account_asset_id': self.company_data['default_account_assets'].id, + 'account_depreciation_id': self.company_data['default_account_assets'].id, + 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, + 'journal_id': self.company_data['default_journal_misc'].id, + }) + asset.validate() + + expense_account_copy = self.company_data['default_account_expense'].copy() + + disposal_action_view = self.env['asset.modify'].create({ + 'asset_id': asset.id, + 'modify_action': 'dispose', + 'loss_account_id': expense_account_copy.id, + 'date': fields.Date.today() + relativedelta(days=-1) + }).sell_dispose() + + report = self.env.ref('at_accounting.assets_report') + options = self._generate_options(report, '2021-01-01', '2021-12-31') + + # The disposal move is in draft and should not be considered (depreciation and book value) + # Values are: name, assets_before, assets+, assets-, assets_after, depreciation_before, depreciation+, depreciation-, depreciation_after, book_value + expected_values_asset_disposal_unposted = [ + ("test asset", 1000.0, 0.0, 0, 1000.0, 400.0, 100.0, 0.0, 500.0, 500.0), + ] + + self.assertLinesValues(report._get_lines(options)[2:3], [0, 5, 6, 7, 8, 9, 10, 11, 12, 13], expected_values_asset_disposal_unposted, options) + + self.env['account.move'].browse(disposal_action_view.get('res_id')).action_post() + + expected_values_asset_disposal_posted = [ + ("test asset", 1000.0, 0.0, 1000.0, 0.0, 400.0, 100.0, 500.0, 0.0, 0.0), + ] + + self.assertLinesValues(report._get_lines(options)[2:3], [0, 5, 6, 7, 8, 9, 10, 11, 12, 13], expected_values_asset_disposal_posted, options) + + def test_depreciation_schedule_disposal_move_unposted_with_non_depreciable_value(self): + """ + Test the computation of values when disposing an asset with non-depreciable value, and the difference if the disposal move is posted + """ + asset = self.env['account.asset'].create({ + 'name': 'test asset', + 'method': 'linear', + 'original_value': 10000, + 'salvage_value': 8000, + 'method_number': 24, + 'method_period': '1', + 'acquisition_date': fields.Date.today() + relativedelta(months=-1, day=1), + 'account_asset_id': self.company_data['default_account_assets'].id, + 'account_depreciation_id': self.company_data['default_account_assets'].id, + 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, + 'journal_id': self.company_data['default_journal_misc'].id, + }) + asset.validate() + + report = self.env.ref('at_accounting.assets_report') + + options = self._generate_options(report, '2021-07-01', '2021-07-31') + + expected_values_asset_disposal_unposted = [ + ("test asset", 10000.0, 0.0, 0.0, 10000.0, 83.33, 0.0, 0.0, 83.33, 9916.67), + ] + + self.assertLinesValues(report._get_lines(options)[2:3], [0, 5, 6, 7, 8, 9, 10, 11, 12, 13], expected_values_asset_disposal_unposted, options) + + expense_account_copy = self.company_data['default_account_expense'].copy() + + disposal_action_view = self.env['asset.modify'].create({ + 'asset_id': asset.id, + 'modify_action': 'dispose', + 'loss_account_id': expense_account_copy.id, + 'date': fields.Date.today() + }).sell_dispose() + + expected_values_asset_disposal_unposted = [ + ("test asset", 10000.0, 0.0, 0.0, 10000.0, 83.33, 2.69, 0.0, 86.02, 9913.98), + ] + + self.assertLinesValues(report._get_lines(options)[2:3], [0, 5, 6, 7, 8, 9, 10, 11, 12, 13], expected_values_asset_disposal_unposted, options) + + self.env['account.move'].browse(disposal_action_view['res_id']).action_post() + + expected_values_asset_disposal_posted = [ + ("test asset", 10000.0, 0.0, 10000.0, 0.0, 83.33, 2.69, 86.02, 0.0, 0.0), + ] + + self.assertLinesValues(report._get_lines(options)[2:3], [0, 5, 6, 7, 8, 9, 10, 11, 12, 13], expected_values_asset_disposal_posted, options) + + def test_asset_analytic_on_lines(self): + CEO_car = self.env['account.asset'].create({ + 'salvage_value': 2000.0, + 'state': 'open', + 'method_period': '12', + 'method_number': 5, + 'name': "CEO's Car", + 'original_value': 12000.0, + 'model_id': self.account_asset_model_fixedassets.id, + 'acquisition_date': '2020-01-01', + }) + CEO_car._onchange_model_id() + CEO_car.method_number = 5 + CEO_car.analytic_distribution = {self.analytic_account.id: 100} + + # In order to test the process of Account Asset, I perform a action to confirm Account Asset. + CEO_car.validate() + + for move in CEO_car.depreciation_move_ids: + self.assertRecordValues(move.line_ids, [ + { + 'analytic_distribution': {str(self.analytic_account.id): 100}, + }, + { + 'analytic_distribution': {str(self.analytic_account.id): 100}, + }, + ]) + + CEO_car.analytic_distribution = {str(self.analytic_account.id): 200} + + # Only draft moves should have a changed analytic distribution + for move in CEO_car.depreciation_move_ids.filtered(lambda m: m.state == 'posted'): + self.assertRecordValues(move.line_ids, [ + { + 'analytic_distribution': {str(self.analytic_account.id): 100}, + }, + { + 'analytic_distribution': {str(self.analytic_account.id): 100}, + }, + ]) + + for move in CEO_car.depreciation_move_ids.filtered(lambda m: m.state == 'draft'): + self.assertRecordValues(move.line_ids, [ + { + 'analytic_distribution': {str(self.analytic_account.id): 200}, + }, + { + 'analytic_distribution': {str(self.analytic_account.id): 200}, + }, + ]) + + + def test_asset_analytic_filter(self): + """ + Test that the analytic filter works correctly. + """ + truck_b = self.truck.copy() + truck_b.acquisition_date = self.truck.acquisition_date + truck_b.validate() + self.truck.analytic_distribution = {self.analytic_account.id: 100} + self.env['account.move']._autopost_draft_entries() + + self.env.company.totals_below_sections = False + report = self.env.ref('at_accounting.assets_report') + + # No prefix group, no group by account + options = self._generate_options(report, '2021-01-01', '2021-12-31', default_options={'assets_grouping_field': 'none', 'unfold_all': False}) + + # without Analytic Filter + self.assertLinesValues( + # pylint: disable=C0326 + report._get_lines(options), + # Name Assets/start Assets/+ Assets/- Assets/end Depreciation/start Depreciation/+ Depreciation/- Depreciation/end Book Value + [ 0, 5, 6, 7, 8, 9, 10, 11, 12, 13], + [ + ('truck', 10000, 0, 0, 10000, 4500, 0, 0, 4500, 5500,), + ('truck (copy)', 10000, 0, 0, 10000, 4500, 0, 0, 4500, 5500,), + ('Total', 20000, 0, 0, 20000, 9000, 0, 0, 9000, 11000,), + ], + options + ) + # with Analytic Filter + options['analytic_accounts'] = [self.analytic_account.id] + self.assertLinesValues( + # pylint: disable=C0326 + report._get_lines(options), + # Name Assets/start Assets/+ Assets/- Assets/end Depreciation/start Depreciation/+ Depreciation/- Depreciation/end Book Value + [ 0, 5, 6, 7, 8, 9, 10, 11, 12, 13], + [ + ('truck', 10000, 0, 0, 10000, 4500, 0, 0, 4500, 5500,), + ('Total', 10000, 0, 0, 10000, 4500, 0, 0, 4500, 5500,), + ], + options + ) + + def test_asset_analytic_groupby(self): + """ + Test that the analytic groupby works correctly. + """ + truck_b = self.truck.copy() + truck_b.acquisition_date = self.truck.acquisition_date + truck_b.validate() + self.truck.analytic_distribution = {self.analytic_account.id: 100} + self.env['account.move']._autopost_draft_entries() + + self.env.company.totals_below_sections = False + report = self.env.ref('at_accounting.assets_report') + report.filter_analytic_groupby = True + + # No prefix group, no group by account + options = self._generate_options(report, '2021-01-01', '2021-12-31', default_options={'assets_grouping_field': 'none', 'unfold_all': False}) + + # without Analytic Groupby + self.assertLinesValues( + # pylint: disable=C0326 + report._get_lines(options), + # Name Assets/start Assets/+ Assets/- Assets/end Depreciation/start Depreciation/+ Depreciation/- Depreciation/end Book Value + [ 0, 5, 6, 7, 8, 9, 10, 11, 12, 13], + [ + ('truck', 10000, 0, 0, 10000, 4500, 0, 0, 4500, 5500,), + ('truck (copy)', 10000, 0, 0, 10000, 4500, 0, 0, 4500, 5500,), + ('Total', 20000, 0, 0, 20000, 9000, 0, 0, 9000, 11000,), + ], + options + ) + # with Analytic Groupby + options = self._generate_options(report, '2021-01-01', '2021-12-31', default_options={ + 'assets_grouping_field': 'none', + 'unfold_all': False, + 'analytic_accounts_groupby': [self.analytic_account.id], + }) + self.assertLinesValues( + # pylint: disable=C0326 + report._get_lines(options), + # Group | ANALYTIC | | ALL | + # Name Assets/start Assets/+ Assets/- Assets/end Depreciation/start Depreciation/+ Depreciation/- Depreciation/end Book Value Assets/start Assets/+ Assets/- Assets/end Depreciation/start Depreciation/+ Depreciation/- Depreciation/end Book Value + [ 0, 5, 6, 7, 8, 9, 10, 11, 12, 13, 18, 19, 20, 21, 22, 23, 24, 25, 26], + [ + ('truck', 10000, 0, 0, 10000, 4500, 0, 0, 4500, 5500, 10000, 0, 0, 10000, 4500, 0, 0, 4500, 5500), + ('truck (copy)', '', '', '', '', '', '', '', '', '', 10000, 0, 0, 10000, 4500, 0, 0, 4500, 5500), + ('Total', 10000, 0, 0, 10000, 4500, 0, 0, 4500, 5500, 20000, 0, 0, 20000, 9000, 0, 0, 9000, 11000), + ], + options + ) + + def test_depreciation_schedule_report_first_depreciation(self): + """Test that the depreciation schedule report displays the correct first depreciation date.""" + # check that the truck's first depreciation date is correct: + # the truck has a yearly linear depreciation and it's prorate_date is 2015-01-01 + # therefore we expect it's first depreciation date to be the last day of 2015 + + today = fields.Date.today() + report = self.env.ref('at_accounting.assets_report') + options = self._generate_options(report, today + relativedelta(years=-6, month=1, day=1), today + relativedelta(years=+4, month=12, day=31)) + lines = report._get_lines({**options, **{'unfold_all': False, 'all_entries': True}}) + + self.assertEqual(lines[1]['columns'][1]['name'], '12/31/2015') + + def test_asset_modify_sell_multicurrency(self): + """ Test that the closing invoice's currency is taken into account when selling an asset. """ + closing_invoice = self.env['account.move'].create({ + 'move_type': 'out_invoice', + 'currency_id': self.other_currency.id, + 'invoice_line_ids': [Command.create({'price_unit': 5000})] + }) + self.env['asset.modify'].create({ + 'asset_id': self.truck.id, + 'invoice_line_ids': closing_invoice.invoice_line_ids, + 'date': fields.Date.today() + relativedelta(months=-6, days=-1), + 'modify_action': 'sell', + }).sell_dispose() + + closing_move = self.truck.depreciation_move_ids.filtered(lambda l: l.state == 'draft') + + self.assertRecordValues(closing_move.line_ids, [{ + 'debit': 0, + 'credit': 10000, + 'account_id': self.truck.account_asset_id.id, + }, { + 'debit': 4500, + 'credit': 0, + 'account_id': self.truck.account_depreciation_id.id, + }, { + 'debit': 2500, + 'credit': 0, + 'account_id': closing_invoice.invoice_line_ids.account_id.id, + }, { + 'debit': 3000, + 'credit': 0, + 'account_id': self.env.company.loss_account_id.id, + }]) + + def test_depreciation_schedule_prefix_groups(self): + asset_group = self.env['account.asset.group'].create({'name': 'Odoo Office'}) + for i in range(1, 3): + asset = self.env['account.asset'].create({ + 'method_period': '12', + 'method_number': 4, + 'name': f"Asset {i}", + 'original_value': i * 100.0, + 'acquisition_date': fields.Date.today() - relativedelta(years=3), + 'account_asset_id': self.company_data['default_account_assets'].id, + 'asset_group_id': asset_group.id, + 'account_depreciation_id': self.company_data['default_account_assets'].copy().id, + 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, + 'journal_id': self.company_data['default_journal_misc'].id, + 'prorata_computation_type': 'none', + }) + asset.validate() + + self.env['account.move']._autopost_draft_entries() + + self.env.company.totals_below_sections = False + report = self.env.ref('at_accounting.assets_report') + + # No prefix group, no group by account + options = self._generate_options(report, '2021-01-01', '2021-12-31', default_options={'assets_grouping_field': 'none'}) + self.assertLinesValues( + # pylint: disable=C0326 + report._get_lines(options), + # Name Assets/start Assets/+ Assets/- Assets/end Depreciation/start Depreciation/+ Depreciation/- Depreciation/end Book Value + [ 0, 5, 6, 7, 8, 9, 10, 11, 12, 13], + [ + ('truck', 10000, 0, 0, 10000, 4500, 0, 0, 4500, 5500,), + ('Asset 1', 100, 0, 0, 100, 75, 0, 0, 75, 25,), + ('Asset 2', 200, 0, 0, 200, 150, 0, 0, 150, 50,), + ('Total', 10300, 0, 0, 10300, 4725, 0, 0, 4725, 5575,), + ], + options, + ) + + # No prefix group, group by account + options = self._generate_options(report, '2021-01-01', '2021-12-31', default_options={'assets_grouping_field': 'account_id'}) + options['unfold_all'] = True + self.assertLinesValues( + # pylint: disable=C0326 + report._get_lines(options), + # Name Assets/start Assets/+ Assets/- Assets/end Depreciation/start Depreciation/+ Depreciation/- Depreciation/end Book Value + [ 0, 5, 6, 7, 8, 9, 10, 11, 12, 13], + [ + ('151000 Fixed Asset', 10300, 0, 0, 10300, 4725, 0, 0, 4725, 5575,), + ('truck', 10000, 0, 0, 10000, 4500, 0, 0, 4500, 5500,), + ('Asset 1', 100, 0, 0, 100, 75, 0, 0, 75, 25,), + ('Asset 2', 200, 0, 0, 200, 150, 0, 0, 150, 50,), + ('Total', 10300, 0, 0, 10300, 4725, 0, 0, 4725, 5575,), + ], + options, + ) + + report.prefix_groups_threshold = 3 + # Prefix group, no group by account + options = self._generate_options(report, '2021-01-01', '2021-12-31', default_options={'assets_grouping_field': 'none', 'unfold_all': True}) + options['unfold_all'] = True + self.assertLinesValues( + # pylint: disable=C0326 + report._get_lines(options), + # Name Assets/start Assets/+ Assets/- Assets/end Depreciation/start Depreciation/+ Depreciation/- Depreciation/end Book Value + [ 0, 5, 6, 7, 8, 9, 10, 11, 12, 13], + [ + ('A (2 lines)', 300, 0, 0, 300, 225, 0, 0, 225, 75,), + ('Asset 1', 100, 0, 0, 100, 75, 0, 0, 75, 25,), + ('Asset 2', 200, 0, 0, 200, 150, 0, 0, 150, 50,), + ('T (1 line)', 10000, 0, 0, 10000, 4500, 0, 0, 4500, 5500,), + ('truck', 10000, 0, 0, 10000, 4500, 0, 0, 4500, 5500,), + ('Total', 10300, 0, 0, 10300, 4725, 0, 0, 4725, 5575,), + ], + options, + ) + + # Prefix group, group by account + options = self._generate_options(report, '2021-01-01', '2021-12-31', default_options={'assets_grouping_field': 'account_id', 'unfold_all': True}) + options['unfold_all'] = True + self.assertLinesValues( + # pylint: disable=C0326 + report._get_lines(options), + # Name Assets/start Assets/+ Assets/- Assets/end Depreciation/start Depreciation/+ Depreciation/- Depreciation/end Book Value + [ 0, 5, 6, 7, 8, 9, 10, 11, 12, 13], + [ + ('151000 Fixed Asset', 10300, 0, 0, 10300, 4725, 0, 0, 4725, 5575,), + ('A (2 lines)', 300, 0, 0, 300, 225, 0, 0, 225, 75,), + ('Asset 1', 100, 0, 0, 100, 75, 0, 0, 75, 25,), + ('Asset 2', 200, 0, 0, 200, 150, 0, 0, 150, 50,), + ('T (1 line)', 10000, 0, 0, 10000, 4500, 0, 0, 4500, 5500,), + ('truck', 10000, 0, 0, 10000, 4500, 0, 0, 4500, 5500,), + ('Total', 10300, 0, 0, 10300, 4725, 0, 0, 4725, 5575,), + ], + options, + ) + + # No prefix group, group by asset group + options = self._generate_options(report, '2021-01-01', '2021-12-31', default_options={'assets_grouping_field': 'asset_group_id'}) + options['unfold_all'] = True + self.assertLinesValues( + # pylint: disable=C0326 + report._get_lines(options), + # Name Assets/start Assets/+ Assets/- Assets/end Depreciation/start Depreciation/+ Depreciation/- Depreciation/end Book Value + [ 0, 5, 6, 7, 8, 9, 10, 11, 12, 13], + [ + ('(No Asset Group)', 10000, 0, 0, 10000, 4500, 0, 0, 4500, 5500), + ('truck', 10000, 0, 0, 10000, 4500, 0, 0, 4500, 5500), + ('Odoo Office', 300, 0, 0, 300, 225, 0, 0, 225, 75), + ('Asset 1', 100, 0, 0, 100, 75, 0, 0, 75, 25), + ('Asset 2', 200, 0, 0, 200, 150, 0, 0, 150, 50), + ('Total', 10300, 0, 0, 10300, 4725, 0, 0, 4725, 5575), + ], + options, + ) + + def test_archive_asset_model(self): + """ Test that we can archive an asset model. """ + self.account_asset_model_fixedassets.active = False + self.assertFalse(self.account_asset_model_fixedassets.active) + + def test_asset_increase_with_lock_year(self): + """ Test the dates at which the moves are posted even with increase, with lock date""" + self.company_data['company'].fiscalyear_lock_date = fields.Date.to_date('2021-03-01') + + asset = self.env['account.asset'].create({ + 'account_asset_id': self.company_data['default_account_assets'].id, + 'account_depreciation_id': self.company_data['default_account_assets'].copy().id, + 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, + 'journal_id': self.company_data['default_journal_misc'].id, + 'name': 'Car', + 'acquisition_date': fields.Date.today() + relativedelta(months=-6), + 'original_value': 12000, + 'method_number': 12, + 'method_period': '1', + 'method': 'linear', + }) + + asset.validate() + + self.assertRecordValues( + asset.depreciation_move_ids.sorted(lambda l: (l.date, l.id)), + [ + {'date': fields.Date.to_date('2021-03-31')}, + {'date': fields.Date.to_date('2021-03-31')}, + {'date': fields.Date.to_date('2021-03-31')}, + {'date': fields.Date.to_date('2021-04-30')}, + {'date': fields.Date.to_date('2021-05-31')}, + {'date': fields.Date.to_date('2021-06-30')}, + {'date': fields.Date.to_date('2021-07-31')}, + {'date': fields.Date.to_date('2021-08-31')}, + {'date': fields.Date.to_date('2021-09-30')}, + {'date': fields.Date.to_date('2021-10-31')}, + {'date': fields.Date.to_date('2021-11-30')}, + {'date': fields.Date.to_date('2021-12-31')} + ] + ) + + self.assertEqual(asset.book_value, 6000) + + self.env['asset.modify'].create({ + 'asset_id': asset.id, + 'name': 'Test increase with lock date', + 'value_residual': 8000.0, + 'date': fields.Date.today() + relativedelta(days=-1), + "account_asset_counterpart_id": self.assert_counterpart_account_id, + }).modify() + + self.assertEqual(asset.book_value, 8000) + + self.assertRecordValues( + asset.children_ids.depreciation_move_ids.sorted(lambda dep: (dep.date, dep.id)), + [ + {'date': fields.Date.to_date('2021-07-31'), 'depreciation_value': 333.33}, + {'date': fields.Date.to_date('2021-08-31'), 'depreciation_value': 333.34}, + {'date': fields.Date.to_date('2021-09-30'), 'depreciation_value': 333.33}, + {'date': fields.Date.to_date('2021-10-31'), 'depreciation_value': 333.33}, + {'date': fields.Date.to_date('2021-11-30'), 'depreciation_value': 333.34}, + {'date': fields.Date.to_date('2021-12-31'), 'depreciation_value': 333.33} + ] + ) + + def test_asset_decrease_with_lock_year(self): + """ Test the dates and values for the moves that are posted with decrease and lock date""" + self.company_data['company'].fiscalyear_lock_date = fields.Date.to_date('2021-03-01') + + asset = self.env['account.asset'].create({ + 'account_asset_id': self.company_data['default_account_assets'].id, + 'account_depreciation_id': self.company_data['default_account_assets'].copy().id, + 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, + 'journal_id': self.company_data['default_journal_misc'].id, + 'name': 'Car', + 'acquisition_date': fields.Date.today() + relativedelta(months=-6), + 'original_value': 12000, + 'method_number': 12, + 'method_period': '1', + 'method': 'linear', + }) + + asset.validate() + + self.assertEqual(asset.book_value, 6000) + + self.env['asset.modify'].create({ + 'asset_id': asset.id, + 'name': 'Test decrease with lock date', + 'value_residual': 4000.0, + 'date': fields.Date.today() + relativedelta(days=-1), + "account_asset_counterpart_id": self.assert_counterpart_account_id, + }).modify() + + self.assertEqual(asset.book_value, 4000) + + self.assertRecordValues( + asset.depreciation_move_ids.sorted(lambda dep: (dep.date, dep.id)), + [ + {'date': fields.Date.to_date('2021-03-31'), 'depreciation_value': 1000}, + {'date': fields.Date.to_date('2021-03-31'), 'depreciation_value': 1000}, + {'date': fields.Date.to_date('2021-03-31'), 'depreciation_value': 1000}, + {'date': fields.Date.to_date('2021-04-30'), 'depreciation_value': 1000}, + {'date': fields.Date.to_date('2021-05-31'), 'depreciation_value': 1000}, + {'date': fields.Date.to_date('2021-06-30'), 'depreciation_value': 1000}, + {'date': fields.Date.to_date('2021-06-30'), 'depreciation_value': 2000}, + {'date': fields.Date.to_date('2021-07-31'), 'depreciation_value': 666.67}, + {'date': fields.Date.to_date('2021-08-31'), 'depreciation_value': 666.66}, + {'date': fields.Date.to_date('2021-09-30'), 'depreciation_value': 666.67}, + {'date': fields.Date.to_date('2021-10-31'), 'depreciation_value': 666.67}, + {'date': fields.Date.to_date('2021-11-30'), 'depreciation_value': 666.66}, + {'date': fields.Date.to_date('2021-12-31'), 'depreciation_value': 666.67} + ] + ) + + def test_asset_onchange_model(self): + """ + Test the changes of account_asset_id when changing asset models + """ + account_asset = self.company_data['default_account_assets'].copy() + asset_model = self.env['account.asset'].create({ + 'name': 'test model', + 'state': 'model', + 'active': True, + 'method': 'linear', + 'method_number': 5, + 'method_period': '1', + 'prorata_computation_type': 'none', + 'account_depreciation_id': self.company_data['default_account_assets'].id, + 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, + 'account_asset_id': at_accounting.id, + 'journal_id': self.company_data['default_journal_misc'].id, + }) + + asset_model_with_account = self.env['account.asset'].create({ + 'name': 'test model with account', + 'state': 'model', + 'active': True, + 'method': 'linear', + 'method_number': 5, + 'method_period': '1', + 'prorata_computation_type': 'none', + 'account_depreciation_id': self.company_data['default_account_assets'].id, + 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, + 'journal_id': self.company_data['default_journal_misc'].id, + }) + + asset_form = Form(self.env['account.asset']) + asset_form.name = "Test Asset" + asset_form.original_value = 10000 + asset_form.model_id = asset_model + + self.assertEqual(asset_form.account_asset_id, account_asset, "The account_asset_id should be the one from the model") + + asset_form.model_id = asset_model_with_account + self.assertEqual(asset_form.account_asset_id, self.company_data['default_account_assets'], "The account_asset_id should be computed from the depreciation account from the model") + + other_account_on_bill = self.company_data['default_account_assets'].copy() + other_account_on_bill.create_asset = 'draft' + other_account_on_bill.asset_model_ids = asset_model + invoice = self.env['account.move'].create({ + 'move_type': 'in_invoice', + 'invoice_date': '2020-12-31', + 'partner_id': self.partner_a.id, + 'invoice_line_ids': [ + (0, 0, { + 'name': 'A beautiful small bomb', + 'account_id': other_account_on_bill.id, + 'price_unit': 200.0, + 'quantity': 1, + }), + ], + }) + invoice.action_post() + + self.assertEqual(invoice.asset_ids.account_asset_id, other_account_on_bill, + "The account should be the one from the bill, not the model") + + asset_form = Form(invoice.asset_ids) + asset_form.model_id = asset_model + + self.assertEqual(asset_form.account_asset_id, other_account_on_bill, "We keep the account from the bill") + + def test_asset_reevaluation_degressive_linear(self): + """ Tests the reevaluation of an asset in degressive_then_linear with a gross increase""" + asset = self.env['account.asset'].create({ + 'method_period': '12', + 'method_number': 5, + 'name': "Car with purple sticker", + 'original_value': 10000.0, + 'acquisition_date': fields.Date.today() - relativedelta(years=2), + 'account_asset_id': self.company_data['default_account_assets'].id, + 'account_depreciation_id': self.company_data['default_account_assets'].copy().id, + 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, + 'journal_id': self.company_data['default_journal_misc'].id, + 'prorata_computation_type': 'none', + 'method': 'degressive_then_linear', + 'method_progress_factor': 0.4, + }) + asset.validate() + self.assertRecordValues(asset.depreciation_move_ids, [{ + 'depreciation_value': 4000, + 'asset_remaining_value': 6000, + 'state': 'posted', + }, { + 'depreciation_value': 2400, + 'asset_remaining_value': 3600, + 'state': 'posted', + }, { + 'depreciation_value': 2000, + 'asset_remaining_value': 1600, + 'state': 'draft', + }, { + 'depreciation_value': 1600, + 'asset_remaining_value': 0, + 'state': 'draft', + }]) + self.env['asset.modify'].create({ + 'name': "Inflation made it take 20%!", + 'date': fields.Date.today() + relativedelta(months=-6, days=-1), + 'asset_id': asset.id, + 'value_residual': 5600, + "account_asset_counterpart_id": self.assert_counterpart_account_id, + }).modify() + self.assertRecordValues(asset.children_ids[0].depreciation_move_ids.sorted(lambda mv: (mv.date, mv.id)), [{ + # (2000 + 2000*6400/3600) / 5 + 'depreciation_value': 1111.11, + 'asset_remaining_value': 888.89, + 'state': 'draft', + }, { + 'depreciation_value': 888.89, + 'asset_remaining_value': 0, + 'state': 'draft', + }]) + + def test_asset_move_type(self): + """ Test the field asset_move_type set on account.move describing the + relation that a move can have towards an asset + """ + asset_account_id = self.company_data['default_account_assets'].id + + bill = self.env['account.move'].create([ + { + 'move_type': 'in_invoice', + 'invoice_date': fields.Date.today() + relativedelta(months=-6, days=-1), + 'date': fields.Date.today() + relativedelta(months=-6, days=-1), + 'partner_id': self.partner_a.id, + 'invoice_line_ids': [Command.create({ + 'name': 'Truck', + 'account_id': asset_account_id, + 'quantity': 1.0, + 'price_unit': 1000.0, + 'tax_ids': [Command.set(self.company_data['default_tax_sale'].ids)], + })], + }, + ]) + bill.action_post() + asset_line = bill.line_ids.filtered(lambda x: x.account_id.id == asset_account_id) + asset_form = Form(self.env['account.asset'].with_context(default_original_move_line_ids=asset_line.ids)) + asset_form.original_move_line_ids = asset_line + asset_form.account_depreciation_expense_id = self.company_data['default_account_expense'] + car = asset_form.save() + car.validate() + + # All depreciation move must be defined as depreciation + self.assertTrue(all(car.depreciation_move_ids.mapped(lambda m: m.asset_move_type == 'depreciation'))) + + # Negative revaluation + self.env['asset.modify'].create({ + 'name': 'Little scratch :(', + 'asset_id': car.id, + 'value_residual': car.book_value - 150, + 'date': fields.Date.today(), + }).modify() + + # Ensure that the added depreciation moves are one 'depreciation' and the other is 'negative_revaluation' + added_move_on_revaluation = car.depreciation_move_ids.filtered(lambda m: m.date == fields.Date.today()) + self.assertRecordValues(added_move_on_revaluation.sorted(lambda mv: mv.id), [ + {'asset_move_type': 'depreciation'}, + {'asset_move_type': 'negative_revaluation'} + ]) + + # Sell + closing_invoice = self.env['account.move'].create({ + 'move_type': 'out_invoice', + 'invoice_line_ids': [Command.create( + {'price_unit': car.book_value + 100} # selling price: 849.46, net_gain_on_sale: 100.45 + )] + }) + + self.env['asset.modify'].create({ + 'asset_id': car.id, + 'modify_action': 'sell', + 'invoice_line_ids': closing_invoice.invoice_line_ids, + 'date': fields.Date.today(), + }).sell_dispose() + selling_move = car.depreciation_move_ids.filtered(lambda l: l.state == 'draft') + selling_move.action_post() + + # Ensure that the added depreciation moves are one 'depreciation' and the other is 'sale' + added_move_on_sale = car.depreciation_move_ids.filtered(lambda m: m.date == fields.Date.today()) - added_move_on_revaluation + self.assertTrue(added_move_on_sale.asset_move_type == 'sale') + self.assertEqual(car.net_gain_on_sale, 100) + + # Create new asset to test positive revaluation and disposal + new_car = car.copy() + new_car.validate() + + # Positive revaluation + self.env['asset.modify'].create({ + 'name': 'New beautiful sticker :D', + 'asset_id': new_car.id, + 'value_residual': new_car.book_value + 50, + 'salvage_value': 0, + 'date': fields.Date.today(), + "account_asset_counterpart_id": self.assert_counterpart_account_id, + }).modify() + + self.assertEqual( + new_car.children_ids.original_move_line_ids.move_id.asset_move_type, + 'positive_revaluation', + "the original move of the child asset is set as 'positive_revaluation'" + ) + + disposal_action_view = self.env['asset.modify'].create({ + 'asset_id': new_car.id, + 'modify_action': 'dispose', + 'date': fields.Date.today() + }).sell_dispose() + + self.env['account.move'].browse(disposal_action_view['res_id']).action_post() + self.assertEqual(self.env['account.move'].browse(disposal_action_view['res_id']).asset_move_type, 'disposal') + + def test_asset_already_depreciated(self): + asset = self.env['account.asset'].create({ + 'method_period': '12', + 'method_number': 5, + 'name': "Car with purple sticker", + 'original_value': 10000.0, + 'acquisition_date': fields.Date.today() - relativedelta(years=1), + 'account_asset_id': self.company_data['default_account_assets'].id, + 'account_depreciation_id': self.company_data['default_account_assets'].copy().id, + 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, + 'journal_id': self.company_data['default_journal_misc'].id, + 'prorata_computation_type': 'none', + 'already_depreciated_amount_import': 3000, + }) + asset.validate() + + self.env['asset.modify'].create({ + 'asset_id': asset.id, + 'date': fields.Date.today() - relativedelta(days=1), + 'name': 'Test reason', + }).modify() + + self.assertRecordValues(asset.depreciation_move_ids, [{ + 'depreciation_value': 1000, + 'date': fields.Date.to_date('2021-12-31'), + }, { + 'depreciation_value': 2000, + 'date': fields.Date.to_date('2022-12-31'), + }, { + 'depreciation_value': 2000, + 'date': fields.Date.to_date('2023-12-31'), + }, { + 'depreciation_value': 2000, + 'date': fields.Date.to_date('2024-12-31'), + }, + ]) + + fully_depreciated_asset = self.env['account.asset'].create({ + 'method_period': '12', + 'method_number': 5, + 'name': "Car with purple sticker", + 'original_value': 10000.0, + 'acquisition_date': fields.Date.today() - relativedelta(years=2), + 'account_asset_id': self.company_data['default_account_assets'].id, + 'account_depreciation_id': self.company_data['default_account_assets'].copy().id, + 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, + 'journal_id': self.company_data['default_journal_misc'].id, + 'prorata_computation_type': 'none', + 'salvage_value': 4000, + 'already_depreciated_amount_import': 6000, + }) + fully_depreciated_asset.validate() + + self.env['asset.modify'].create({ + 'asset_id': fully_depreciated_asset.id, + 'date': fields.Date.today(), + 'modify_action': 'dispose', + }).sell_dispose() + self.assertEqual(len(fully_depreciated_asset.depreciation_move_ids), 1, "Only the disposal should be created") + + def test_asset_acquisition_date_from_bill(self): + """Test that the invoice date is used as acquisition date instead of date""" + self.company_data['default_account_assets'].create_asset = 'draft' + self.company_data['default_account_assets'].asset_model_ids = self.account_asset_model_fixedassets + + bill = self.env['account.move'].with_context(asset_type='purchase').create({ + 'move_type': 'in_invoice', + 'partner_id': self.partner_a.id, + 'date': '2020-06-15', + 'invoice_date': '2020-06-01', + 'invoice_line_ids': [Command.create({ + 'name': 'Insurance claim', + 'account_id': self.company_data['default_account_assets'].id, + 'price_unit': 450, + 'quantity': 1, + })], + }) + bill.action_post() + asset = bill.asset_ids + self.assertEqual(asset.acquisition_date, bill.invoice_date) + + def test_asset_write_multi_company(self): + assets = self.env['account.asset'].create([ + { + 'company_id': company_data['company'].id, + 'name': 'test asset', + } for company_data in [self.company_data, self.company_data_2] + ]) + self.assertEqual(assets[0].company_id, self.company_data['company']) + self.assertEqual(assets[1].company_id, self.company_data_2['company']) + assets.validate() + + def test_depreciation_moves_company_with_sub_company(self): + """The depreciation moves should have the company of the asset, even in multicompany setup""" + company = self.env.company + branch_x = self.env['res.company'].create({ + 'name': 'Branch X', + 'country_id': company.country_id.id, + 'parent_id': company.id, + }) + + asset_vals = { + 'method_period': '12', + 'method_number': 5, + 'name': "Car with purple sticker", + 'original_value': 10000.0, + 'acquisition_date': fields.Date.today() - relativedelta(years=1), + 'account_asset_id': self.company_data['default_account_assets'].id, + 'account_depreciation_id': self.company_data['default_account_assets'].copy().id, + 'account_depreciation_expense_id': self.company_data['default_account_expense'].id, + 'journal_id': self.company_data['default_journal_misc'].id, + } + + setup_list = [ + {'company_ids': (company + branch_x).ids, 'company_id': branch_x.id}, + {'company_ids': branch_x.ids, 'company_id': branch_x.id}, + {'company_ids': (company + branch_x).ids, 'company_id': company.id}, + {'company_ids': company.ids, 'company_id': company.id}, + ] + + expected_vals_list = [branch_x, branch_x, company, company] + + for setup, expected in zip(setup_list, expected_vals_list): + with self.subTest(setup=setup, expected_company=expected): + self.env.user.write({ + 'company_ids': [Command.set(setup['company_ids'])], + 'company_id': setup['company_id'], + }) + asset = self.env['account.asset'].create(asset_vals) + asset.compute_depreciation_board() + self.assertEqual(asset.depreciation_move_ids.mapped('company_id'), expected)