diff --git a/l10n_it_asset_management/__manifest__.py b/l10n_it_asset_management/__manifest__.py index 12aa1da94dc9..2e2ebc3d299d 100644 --- a/l10n_it_asset_management/__manifest__.py +++ b/l10n_it_asset_management/__manifest__.py @@ -5,7 +5,7 @@ { "name": "ITA - Gestione Cespiti", - "version": "16.0.1.0.0", + "version": "16.0.1.1.0", "category": "Localization/Italy", "summary": "Gestione Cespiti", "author": "Openforce, Odoo Community Association (OCA)", diff --git a/l10n_it_asset_management/data/asset_data.xml b/l10n_it_asset_management/data/asset_data.xml index 44305f64ea84..3eb1ed340c1a 100644 --- a/l10n_it_asset_management/data/asset_data.xml +++ b/l10n_it_asset_management/data/asset_data.xml @@ -37,8 +37,8 @@ - 1 - 1 + 1 + 1 0.5 diff --git a/l10n_it_asset_management/migrations/16.0.1.1.0/pre-migrate.py b/l10n_it_asset_management/migrations/16.0.1.1.0/pre-migrate.py new file mode 100644 index 000000000000..7ba7638b0ef0 --- /dev/null +++ b/l10n_it_asset_management/migrations/16.0.1.1.0/pre-migrate.py @@ -0,0 +1,32 @@ +# Copyright 2024 Simone Rubino - Aion Tech +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from openupgradelib import openupgrade + +MODEL_TO_RENAMED_FIELDS = { + "asset.depreciation.mode.line": [ + ("from_nr", "from_year_nr"), + ("to_nr", "to_year_nr"), + ] +} + + +def _rename_fields(env): + openupgrade.rename_fields( + env, + [ + ( + model_name, + model_name.replace(".", "_"), + field_spec[0], + field_spec[1], + ) + for model_name, field_specs in MODEL_TO_RENAMED_FIELDS.items() + for field_spec in field_specs + ], + ) + + +@openupgrade.migrate() +def migrate(env, version): + _rename_fields(env) diff --git a/l10n_it_asset_management/models/account_fiscal_year.py b/l10n_it_asset_management/models/account_fiscal_year.py index 64d697d50dce..45dd33822e28 100644 --- a/l10n_it_asset_management/models/account_fiscal_year.py +++ b/l10n_it_asset_management/models/account_fiscal_year.py @@ -35,3 +35,25 @@ def get_fiscal_year_by_date_domain(self, date, company=None): if company: domain.append(("company_id", "in", company.ids)) return domain + + @api.model + def _get_passed_years(self, start_date, end_date): + """Find all fiscal years between `start_date` and `end_date`.""" + if start_date and end_date: + overlapping_fiscal_year_domain = self.new( + { + "date_from": start_date, + "date_to": end_date, + } + )._get_overlapping_domain() + # Exclude current record's NewId + # because it is not supported in domains + overlapping_fiscal_year_domain = [ + term if term[0] != "id" else ("id", "!=", 0) + for term in overlapping_fiscal_year_domain + ] + overlapping_fiscal_years = self.search(overlapping_fiscal_year_domain) + passed_years = len(overlapping_fiscal_years) + else: + passed_years = None + return passed_years diff --git a/l10n_it_asset_management/models/asset_depreciation.py b/l10n_it_asset_management/models/asset_depreciation.py index 994d50671c25..98c9d4ad5a9e 100644 --- a/l10n_it_asset_management/models/asset_depreciation.py +++ b/l10n_it_asset_management/models/asset_depreciation.py @@ -289,26 +289,30 @@ def check_before_generate_depreciation_lines(self, dep_date): ) ) - def generate_depreciation_lines(self, dep_date): + def generate_depreciation_lines(self, dep_date, period=None): # Set new date within context if necessary self.check_before_generate_depreciation_lines(dep_date) new_lines = self.env["asset.depreciation.line"] for dep in self: - new_line = dep.generate_depreciation_lines_single(dep_date) + new_line = dep.generate_depreciation_lines_single(dep_date, period=period) if new_line: new_lines |= new_line return new_lines - def generate_depreciation_lines_single(self, dep_date): + def generate_depreciation_lines_single(self, dep_date, period=None): self.ensure_one() res = self.env["asset.depreciation.line"] if self.last_depreciation_date and self.last_depreciation_date > dep_date: return res - dep_nr = self.get_max_depreciation_nr() + 1 - dep = self.with_context(dep_nr=dep_nr, used_asset=self.asset_id.used) - dep_amount = dep.get_depreciation_amount(dep_date) + passed_fiscal_years = self.env["account.fiscal.year"]._get_passed_years( + self.asset_id.purchase_date, dep_date + ) + dep = self.with_context( + passed_fiscal_years=passed_fiscal_years, used_asset=self.asset_id.used + ) + dep_amount = dep.get_depreciation_amount(dep_date, period=period) if not dep_amount: return res dep = dep.with_context(dep_amount=dep_amount) @@ -393,7 +397,7 @@ def get_depreciable_amount(self, dep_date=None): depreciable_amount = 0 return depreciable_amount - def get_depreciation_amount(self, dep_date): + def get_depreciation_amount(self, dep_date, period=None): self.ensure_one() zero_dep_date = self.zero_depreciation_until if zero_dep_date and dep_date <= zero_dep_date: @@ -401,7 +405,7 @@ def get_depreciation_amount(self, dep_date): # Get depreciable amount, multiplier and digits amount = self.get_depreciable_amount(dep_date) - multiplier = self.get_depreciation_amount_multiplier(dep_date) + multiplier = self.get_depreciation_amount_multiplier(dep_date, period=period) digits = self.env["decimal.precision"].precision_get("Account") dep_amount = round(amount * multiplier, digits) @@ -411,12 +415,15 @@ def get_depreciation_amount(self, dep_date): return dep_amount - def get_depreciation_amount_multiplier(self, dep_date): + def get_depreciation_amount_multiplier(self, dep_date, period=None): self.ensure_one() # Base multiplier multiplier = self.percentage / 100 + if period == "month": + multiplier /= 12 + # Update multiplier from depreciation mode data multiplier *= self.mode_id.get_depreciation_amount_multiplier() @@ -503,14 +510,6 @@ def get_dismiss_account_move_vals(self): "move_type": "entry", } - def get_max_depreciation_nr(self): - self.ensure_one() - num_lines = self.line_ids.filtered("requires_depreciation_nr") - nums = num_lines.mapped("depreciation_nr") - if not nums: - nums = [0] - return max(nums) - def get_pro_rata_temporis_dates(self, date): """ Gets useful dates for pro rata temporis computations, according to diff --git a/l10n_it_asset_management/models/asset_depreciation_mode_line.py b/l10n_it_asset_management/models/asset_depreciation_mode_line.py index 55474f8c992d..541489c51ff1 100644 --- a/l10n_it_asset_management/models/asset_depreciation_mode_line.py +++ b/l10n_it_asset_management/models/asset_depreciation_mode_line.py @@ -9,7 +9,7 @@ class AssetDepreciationModeLine(models.Model): _name = "asset.depreciation.mode.line" _description = "Asset Depreciation Mode Line" - _order = "from_nr asc, to_nr asc" + _order = "from_year_nr asc, to_year_nr asc" application = fields.Selection( [("coefficient", "Coefficient"), ("percentage", "Percentage")], @@ -24,8 +24,12 @@ class AssetDepreciationModeLine(models.Model): "res.company", readonly=True, related="mode_id.company_id", string="Company" ) - from_nr = fields.Integer( + from_year_nr = fields.Integer( required=True, + string="From Year", + help="Minimum number of fiscal years passed " + "from asset purchase date " + "to apply this line.", ) mode_id = fields.Many2one( @@ -38,7 +42,12 @@ class AssetDepreciationModeLine(models.Model): percentage = fields.Float() - to_nr = fields.Integer() + to_year_nr = fields.Integer( + string="To Year", + help="Maximum number of fiscal years passed " + "from asset purchase date " + "to apply this line.", + ) @api.onchange("application") def onchange_application(self): @@ -53,13 +62,14 @@ def onchange_application(self): def get_depreciation_amount_multiplier(self): multiplier = 1 - nr = self._context.get("dep_nr") - if nr is None: + passed_fiscal_years = self._context.get("passed_fiscal_years") + if passed_fiscal_years is None: # Cannot compare to any line return multiplier lines = self.filtered( - lambda line: line.from_nr <= nr and (not line.to_nr or line.to_nr >= nr) + lambda line: line.from_year_nr <= passed_fiscal_years + and (not line.to_year_nr or line.to_year_nr >= passed_fiscal_years) ) if not lines: return multiplier diff --git a/l10n_it_asset_management/tests/test_assets_management.py b/l10n_it_asset_management/tests/test_assets_management.py index 4a62c4bb4a1b..9c54de7934d8 100644 --- a/l10n_it_asset_management/tests/test_assets_management.py +++ b/l10n_it_asset_management/tests/test_assets_management.py @@ -167,15 +167,24 @@ def _create_asset(self, asset_date): ) return asset - def _depreciate_asset(self, asset, date_dep): + def _depreciate_asset_wizard(self, asset, date_dep, period="year"): wiz_vals = asset.with_context( **{"allow_reload_window": True} ).launch_wizard_generate_depreciations() wiz = ( self.env["wizard.asset.generate.depreciation"] .with_context(**wiz_vals["context"]) - .create({"date_dep": date_dep}) + .create( + { + "date_dep": date_dep, + "period": period, + } + ) ) + return wiz + + def _depreciate_asset(self, asset, date_dep, period="year"): + wiz = self._depreciate_asset_wizard(asset, date_dep, period) wiz.do_generate() def _create_purchase_invoice(self, invoice_date, tax_ids=False, amount=7000): @@ -212,11 +221,18 @@ def _create_purchase_invoice(self, invoice_date, tax_ids=False, amount=7000): def test_00_create_asset_depreciate_and_sale(self): today = fields.Date.today() + asset = self._create_asset(today + relativedelta(years=-1)) first_depreciation_date = today.replace(month=12, day=31) + relativedelta( years=-1 ) second_depreciation_date = today.replace(month=12, day=31) - asset = self._create_asset(today + relativedelta(years=-1)) + self._generate_fiscal_years( + asset.purchase_date, + max( + first_depreciation_date, + second_depreciation_date, + ), + ) civ_type = self.env.ref("l10n_it_asset_management.ad_type_civilistico") depreciation_id = asset.depreciation_ids.filtered( lambda x: x.type_id == civ_type @@ -475,6 +491,24 @@ def test_03_asset_from_purchase_invoice_increment(self): third_depreciation_date = today.replace(month=12, day=31) + relativedelta( years=-3 ) + # create depreciation for year -2 or -1 should do nothing as asset is totally + # depreciated + fourth_depreciation_date = today.replace(month=12, day=31) + relativedelta( + years=-2 + ) + # create depreciation for current year should depreciate totally (as computed + # value 9000*40% = 3600 is greater than residual value) + current_year_depreciation_date = today.replace(month=12, day=31) + self._generate_fiscal_years( + asset.purchase_date, + max( + first_depreciation_date, + second_depreciation_date, + third_depreciation_date, + fourth_depreciation_date, + current_year_depreciation_date, + ), + ) civ_type = self.env.ref("l10n_it_asset_management.ad_type_civilistico") depreciation_id = asset.depreciation_ids.filtered( lambda x: x.type_id == civ_type @@ -521,16 +555,8 @@ def test_03_asset_from_purchase_invoice_increment(self): ) wiz.link_asset() self.assertAlmostEqual(depreciation_id.amount_depreciable_updated, 9000) - # create depreciation for year -2 or -1 should do nothing as asset is totally - # depreciated - fourth_depreciation_date = today.replace(month=12, day=31) + relativedelta( - years=-2 - ) self._depreciate_asset(asset, fourth_depreciation_date) self.assertAlmostEqual(sum(civ_dep_lines.mapped("amount")), 7000) - # create depreciation for current year should depreciate totally (as computed - # value 9000*40% = 3600 is greater than residual value) - current_year_depreciation_date = today.replace(month=12, day=31) self._depreciate_asset(asset, current_year_depreciation_date) dep_lines = asset.depreciation_ids.line_ids civ_dep_lines = dep_lines.filtered( @@ -574,6 +600,23 @@ def test_04_asset_partial_depreciate_from_purchase_invoice_increment(self): second_depreciation_date = today.replace(month=12, day=31) + relativedelta( years=-3 ) + # create depreciation for year -4 should do nothing as asset is already + # depreciated in a later date + third_depreciation_date = today.replace(month=12, day=31) + relativedelta( + years=-4 + ) + # create depreciation for current year should depreciate totally (as computed + # value 9000*40% = 3600 is greater than residual value) + current_year_depreciation_date = today.replace(month=12, day=31) + self._generate_fiscal_years( + asset.purchase_date, + max( + first_depreciation_date, + second_depreciation_date, + third_depreciation_date, + current_year_depreciation_date, + ), + ) civ_type = self.env.ref("l10n_it_asset_management.ad_type_civilistico") depreciation_id = asset.depreciation_ids.filtered( lambda x: x.type_id == civ_type @@ -619,16 +662,8 @@ def test_04_asset_partial_depreciate_from_purchase_invoice_increment(self): ) wiz.link_asset() self.assertAlmostEqual(depreciation_id.amount_depreciable_updated, 9000) - # create depreciation for year -4 should do nothing as asset is already - # depreciated in a later date - third_depreciation_date = today.replace(month=12, day=31) + relativedelta( - years=-4 - ) self._depreciate_asset(asset, third_depreciation_date) self.assertAlmostEqual(sum(civ_dep_lines.mapped("amount")), 7000 * 0.6) - # create depreciation for current year should depreciate totally (as computed - # value 9000*40% = 3600 is greater than residual value) - current_year_depreciation_date = today.replace(month=12, day=31) self._depreciate_asset(asset, current_year_depreciation_date) dep_lines = asset.depreciation_ids.line_ids self.assertEqual(len(dep_lines), 4) @@ -678,7 +713,7 @@ def _civil_depreciate_asset(self, asset): def _generate_fiscal_years(self, start_date, end_date): fiscal_years = range( start_date.year, - end_date.year, + end_date.year + 1, ) fiscal_years_values = list() for fiscal_year in fiscal_years: @@ -738,3 +773,88 @@ def test_journal_prev_year(self): total = report.report_total_ids self.assertEqual(total.amount_depreciation_fund_curr_year, 1000) self.assertEqual(total.amount_depreciation_fund_prev_year, 1000) + + def test_monthly_depreciation(self): + """ + Monthly depreciation uses 1/12 of the coefficient + of the year the depreciation is in. + """ + # Arrange + purchase_date = date(2019, 1, 1) + asset = self._create_asset(purchase_date) + first_depreciation_date = date(2019, 1, 31) + second_depreciation_date = date(2020, 1, 31) + self._generate_fiscal_years( + asset.purchase_date, + max( + first_depreciation_date, + second_depreciation_date, + ), + ) + civ_depreciation_type = self.env.ref( + "l10n_it_asset_management.ad_type_civilistico" + ) + civ_depreciation = asset.depreciation_ids.filtered( + lambda x: x.type_id == civ_depreciation_type + ) + civ_depreciation.percentage = 12.0 + depreciation_mode = asset.category_id.type_ids.mode_id + # pre-condition + self.assertEqual(asset.purchase_date, purchase_date) + self.assertEqual(civ_depreciation.amount_depreciable, 1000) + self.assertRecordValues( + depreciation_mode.line_ids, + [ + { + "from_year_nr": 1, + "to_year_nr": 1, + "application": "coefficient", + "coefficient": 0.5, + }, + ], + ) + + # Act + self._depreciate_asset(asset, first_depreciation_date, period="month") + self._depreciate_asset(asset, second_depreciation_date, period="month") + + # Assert + self.assertRecordValues( + civ_depreciation.line_ids, + [ + { + "date": first_depreciation_date, + "amount": 5, + }, + { + "date": second_depreciation_date, + "amount": 10, + }, + ], + ) + + def test_missing_fiscal_year_warning(self): + """ + If some years are not configured as fiscal years, + the wizard shows a warning. + """ + # Arrange + purchase_date = date(2019, 1, 1) + asset = self._create_asset(purchase_date) + depreciation_date = date(2020, 1, 1) + + # Act + depreciate_wizard = self._depreciate_asset_wizard(asset, depreciation_date) + + # Assert 1: some fiscal years are missing + self.assertTrue(depreciate_wizard.missing_fiscal_year_warning) + + # Act 2: Generate missing years + self._generate_fiscal_years( + asset.purchase_date, + depreciation_date, + ) + + # Assert 2: no fiscal years are missing + depreciate_wizard = self._depreciate_asset_wizard(asset, depreciation_date) + self.assertFalse(depreciate_wizard.missing_fiscal_year_warning) diff --git a/l10n_it_asset_management/views/asset_depreciation_mode.xml b/l10n_it_asset_management/views/asset_depreciation_mode.xml index 38bb73645c8e..b9100c8a3039 100644 --- a/l10n_it_asset_management/views/asset_depreciation_mode.xml +++ b/l10n_it_asset_management/views/asset_depreciation_mode.xml @@ -47,8 +47,8 @@ > - - + + + +