From 672f0fa70b2c0e87f07408a0bfaf264b901ab97a Mon Sep 17 00:00:00 2001 From: Simone Rubino Date: Wed, 10 Jul 2024 12:26:28 +0200 Subject: [PATCH] [IMP] l10n_it_asset_management: Recharge asset --- .../models/account_move.py | 1 + .../models/asset_accounting_info.py | 1 + .../models/asset_depreciation_line.py | 10 ++ l10n_it_asset_management/tests/common.py | 79 ++++++++-- .../tests/test_assets_management.py | 76 +++++++++- .../wizard/account_move_manage_asset.py | 139 ++++++++++++++++++ .../wizard/account_move_manage_asset_view.xml | 38 +++++ 7 files changed, 330 insertions(+), 14 deletions(-) diff --git a/l10n_it_asset_management/models/account_move.py b/l10n_it_asset_management/models/account_move.py index 6f13756a8516..048384155e8f 100644 --- a/l10n_it_asset_management/models/account_move.py +++ b/l10n_it_asset_management/models/account_move.py @@ -107,6 +107,7 @@ def open_wizard_manage_asset(self): { "default_company_id": self.company_id.id, "default_dismiss_date": self.invoice_date or self.invoice_date_due, + "default_recharge_date": self.invoice_date or self.invoice_date_due, "default_move_ids": [Command.set(self.ids)], "default_move_line_ids": [Command.set(lines.ids)], "default_purchase_date": self.invoice_date or self.invoice_date_due, diff --git a/l10n_it_asset_management/models/asset_accounting_info.py b/l10n_it_asset_management/models/asset_accounting_info.py index 94e3a7c03bfa..bd0d26a710e0 100644 --- a/l10n_it_asset_management/models/asset_accounting_info.py +++ b/l10n_it_asset_management/models/asset_accounting_info.py @@ -51,6 +51,7 @@ class AssetAccountingInfo(models.Model): relation_type = fields.Selection( [ ("create", "Asset Creation"), + ("partial_recharge", "Partial Recharge"), ("update", "Asset Update"), ("partial_dismiss", "Asset Partial Dismissal"), ("dismiss", "Asset Dismissal"), diff --git a/l10n_it_asset_management/models/asset_depreciation_line.py b/l10n_it_asset_management/models/asset_depreciation_line.py index 40296df2b197..2a5d5684ab02 100644 --- a/l10n_it_asset_management/models/asset_depreciation_line.py +++ b/l10n_it_asset_management/models/asset_depreciation_line.py @@ -495,3 +495,13 @@ def post_partial_dismiss_asset(self): ) if to_create_move: to_create_move.generate_account_move() + + def post_partial_recharge_asset(self): + dep = self.mapped("depreciation_id") + dep.ensure_one() + types = ("depreciated", "gain", "loss") + to_create_move = self.filtered( + lambda line: line.needs_account_move() and line.move_type in types + ) + if to_create_move: + to_create_move.generate_account_move() diff --git a/l10n_it_asset_management/tests/common.py b/l10n_it_asset_management/tests/common.py index 54afe33feacf..823e9dbd9a3e 100644 --- a/l10n_it_asset_management/tests/common.py +++ b/l10n_it_asset_management/tests/common.py @@ -6,7 +6,7 @@ from datetime import date from odoo.fields import Command, first -from odoo.tests.common import TransactionCase, Form +from odoo.tests.common import Form, TransactionCase class Common(TransactionCase): @@ -65,6 +65,12 @@ def setUpClass(cls): ], limit=1, ) + cls.sale_journal = cls.env["account.journal"].search( + [ + ("type", "=", "sale"), + ], + limit=1, + ) cls.civilistico_asset_dep_type = cls.env.ref( "l10n_it_asset_management.ad_type_civilistico" @@ -208,6 +214,49 @@ def _create_purchase_invoice(self, invoice_date, tax_ids=False, amount=7000): self.assertEqual(purchase_invoice.state, "posted") return purchase_invoice + def _create_sale_invoice(self, asset, amount=7000, invoice_date=None, post=True): + sale_invoice = self.env["account.move"].create( + { + "move_type": "out_invoice", + "invoice_date": invoice_date, + "partner_id": self.env.ref("base.partner_demo").id, + "journal_id": self.sale_journal.id, + "invoice_line_ids": [ + Command.create( + { + "account_id": asset.category_id.asset_account_id.id, + "quantity": 1, + "price_unit": amount, + }, + ) + ], + } + ) + if post: + sale_invoice.action_post() + return sale_invoice + + def _refund_move(self, move, method="cancel", ref_date=None): + reverse_context = { + "active_model": move._name, + "active_ids": move.ids, + } + refund_wizard_form = Form( + self.env["account.move.reversal"].with_context(**reverse_context) + ) + refund_wizard_form.reason = "test" + if ref_date: + refund_wizard_form.date_mode = "custom" + refund_wizard_form.date = ref_date + refund_wizard_form.refund_method = method + refund_wizard = refund_wizard_form.save() + + refund_action = refund_wizard.reverse_moves() + refund_move = self.env[refund_action["res_model"]].browse( + refund_action["res_id"] + ) + return refund_move + def _civil_depreciate_asset(self, asset): # Keep only one civil depreciation civil_depreciation_type = self.env.ref( @@ -298,15 +347,21 @@ def _create_entry(self, account, amount, post=True): self.assertEqual(entry.move_type, "entry") return entry - def _update_asset(self, entry, asset): - """Execute the wizard on `entry` to update `asset`.""" - wizard_action = entry.open_wizard_manage_asset() - wizard_model = self.env[wizard_action["res_model"]] - wizard_context = wizard_action["context"] + def _link_asset_move(self, move, link_management_type, wiz_values=None): + """Link `asset` to `move` with mode `link_management_type`. + `wiz_values` are values to be set in the wizard. + """ + if wiz_values is None: + wiz_values = {} - wizard_form = Form(wizard_model.with_context(**wizard_context)) - wizard_form.management_type = "update" - wizard_form.asset_id = asset - wizard = wizard_form.save() - - return wizard.link_asset() + wiz_action_values = move.open_wizard_manage_asset() + wiz_form = Form( + self.env["wizard.account.move.manage.asset"].with_context( + **wiz_action_values["context"] + ) + ) + wiz_form.management_type = link_management_type + for field_name, field_value in wiz_values.items(): + setattr(wiz_form, field_name, field_value) + wiz = wiz_form.save() + return wiz.link_asset() diff --git a/l10n_it_asset_management/tests/test_assets_management.py b/l10n_it_asset_management/tests/test_assets_management.py index b41e540e0078..e0c79be9853c 100644 --- a/l10n_it_asset_management/tests/test_assets_management.py +++ b/l10n_it_asset_management/tests/test_assets_management.py @@ -3,6 +3,7 @@ # Copyright 2023 Simone Rubino - Aion Tech # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import datetime from datetime import date from odoo import fields @@ -456,7 +457,13 @@ def test_entry_in_update_asset(self): self.assertFalse(asset.asset_accounting_info_ids) # Act - self._update_asset(entry, asset) + self._link_asset_move( + entry, + "update", + wiz_values={ + "asset_id": asset, + }, + ) # Assert accounting_info = asset.asset_accounting_info_ids @@ -476,7 +483,13 @@ def test_entry_out_update_asset(self): self.assertFalse(asset.asset_accounting_info_ids) # Act - self._update_asset(entry, asset) + self._link_asset_move( + entry, + "update", + wiz_values={ + "asset_id": asset, + }, + ) # Assert accounting_info = asset.asset_accounting_info_ids @@ -508,3 +521,62 @@ 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_purchase_sale_refund_recharge(self): + """A sale refund can be used to restore asset value.""" + # Create with purchase + purchase_amount = 2500 + purchase_invoice = self._create_purchase_invoice( + datetime.date(2020, month=1, day=1), amount=purchase_amount + ) + asset = self._link_asset_move( + purchase_invoice, + "create", + { + "category_id": self.asset_category_1, + "name": "Test recharge asset", + }, + ) + civ_depreciation = asset.depreciation_ids.filtered( + lambda x: x.type_id + == self.env.ref("l10n_it_asset_management.ad_type_civilistico") + ) + self.assertEqual(civ_depreciation.amount_depreciable_updated, purchase_amount) + + # Partial dismiss with sale + asset_account_amount = asset_fund_amount = 1000 + sale_invoice = self._create_sale_invoice( + asset, amount=8000, invoice_date=datetime.date(2020, month=3, day=1) + ) + self._link_asset_move( + sale_invoice, + "partial_dismiss", + wiz_values={ + "asset_id": asset, + "depreciated_fund_amount": asset_account_amount, + "asset_purchase_amount": asset_fund_amount, + }, + ) + self.assertEqual( + civ_depreciation.amount_depreciable_updated, + purchase_amount - asset_account_amount, + ) + + # Refund and recharge + sale_refund = self._refund_move( + sale_invoice, ref_date=datetime.date(2020, month=7, day=1) + ) + recharge_purchase_amount = recharge_fund_amount = 1000 + self._link_asset_move( + sale_refund, + "partial_recharge", + wiz_values={ + "asset_id": asset, + "recharge_purchase_amount": recharge_purchase_amount, + "recharge_fund_amount": recharge_fund_amount, + }, + ) + self.assertEqual( + civ_depreciation.amount_depreciable_updated, + purchase_amount, + ) diff --git a/l10n_it_asset_management/wizard/account_move_manage_asset.py b/l10n_it_asset_management/wizard/account_move_manage_asset.py index 1156aba16558..4c32d61c5167 100644 --- a/l10n_it_asset_management/wizard/account_move_manage_asset.py +++ b/l10n_it_asset_management/wizard/account_move_manage_asset.py @@ -66,6 +66,7 @@ def get_default_move_ids(self): management_type = fields.Selection( [ ("create", "Create New"), + ("partial_recharge", "Partial Recharge"), ("update", "Update Existing"), ("partial_dismiss", "Partial Dismiss"), ("dismiss", "Dismiss Asset"), @@ -104,6 +105,12 @@ def get_default_move_ids(self): used = fields.Boolean() + recharge_date = fields.Date( + default=fields.Date.today(), + ) + recharge_purchase_amount = fields.Monetary() + recharge_fund_amount = fields.Monetary() + # Mapping between move journal type and depreciation line type _move_journal_type_2_dep_line_type = { "purchase": "in", @@ -117,6 +124,7 @@ def get_default_move_ids(self): # Every method used in here must return an asset _management_type_2_method = { "create": lambda w: w.create_asset(), + "partial_recharge": lambda w: w.partial_recharge_asset(), "dismiss": lambda w: w.dismiss_asset(), "partial_dismiss": lambda w: w.partial_dismiss_asset(), "update": lambda w: w.update_asset(), @@ -735,3 +743,134 @@ def update_asset(self): self.check_pre_update_asset() self.asset_id.write(self.get_update_asset_vals()) return self.asset_id + + def check_pre_partial_recharge_asset(self): + self.ensure_one() + asset = self.asset_id + if not asset: + raise ValidationError(_("Please choose an asset before continuing!")) + + move_lines = self.move_line_ids + if not move_lines: + raise ValidationError( + _( + "At least one move line is needed" + " to partial recharge asset %(asset)s!", + asset=asset, + ) + ) + + asset_account = asset.category_id.asset_account_id + if not all(line.account_id == asset_account for line in move_lines): + raise ValidationError( + _( + "You need to choose move lines with account `%(ass_acc)s`" + " if you need them to partial recharge asset `%(ass_name)s`!", + ass_acc=asset_account.display_name, + ass_name=asset.display_name, + ) + ) + + def get_partial_recharge_asset_vals(self): + self.ensure_one() + asset = self.asset_id + currency = self.asset_id.currency_id + recharge_date = self.recharge_date + digits = self.env["decimal.precision"].precision_get("Account") + fund_amt = self.recharge_fund_amount + purchase_amt = self.recharge_purchase_amount + + move = self.move_line_ids.mapped("move_id") + move_nums = move.name + + writeoff = 0 + for line in self.move_line_ids: + writeoff += line.currency_id._convert( + line.credit - line.debit, currency, line.company_id, line.date + ) + writeoff = round(writeoff, digits) + + vals = {"depreciation_ids": []} + for dep in asset.depreciation_ids: + dep_writeoff = writeoff + if dep.pro_rata_temporis: + dep_writeoff *= dep.get_pro_rata_temporis_multiplier( + recharge_date, "std" + ) + + name = _( + "Partial recharge from move(s) %(move_nums)s", + move_nums=move_nums, + ) + + out_line_vals = { + "asset_accounting_info_ids": [ + Command.create( + { + "move_line_id": line.id, + "relation_type": self.management_type, + }, + ) + for line in self.move_line_ids + ], + "amount": purchase_amt, + "date": recharge_date, + "move_type": "in", + "name": name, + } + dep_line_vals = { + "asset_accounting_info_ids": [ + Command.create( + { + "move_line_id": line.id, + "relation_type": self.management_type, + }, + ) + for line in self.move_line_ids + ], + "amount": -fund_amt, + "date": recharge_date, + "move_type": "depreciated", + "name": name, + } + + dep_vals = { + "line_ids": [ + Command.create(out_line_vals), + Command.create(dep_line_vals), + ] + } + + balance = purchase_amt + dep_writeoff - fund_amt + if not float_is_zero(balance, digits): + loss_gain_vals = { + "asset_accounting_info_ids": [ + Command.create( + { + "move_line_id": line.id, + "relation_type": self.management_type, + }, + ) + for line in self.move_line_ids + ], + "amount": abs(balance), + "date": recharge_date, + "move_type": "gain" if balance > 0 else "loss", + "name": name, + } + dep_vals["line_ids"].append(Command.create(loss_gain_vals)) + + vals["depreciation_ids"].append(Command.update(dep.id, dep_vals)) + return vals + + def partial_recharge_asset(self): + """Recharge asset partially and return it.""" + self.ensure_one() + self.check_pre_partial_recharge_asset() + old_dep_lines = self.asset_id.mapped("depreciation_ids.line_ids") + self.asset_id.write(self.get_partial_recharge_asset_vals()) + + for dep in self.asset_id.depreciation_ids: + (dep.line_ids - old_dep_lines).post_partial_dismiss_asset() + + return self.asset_id diff --git a/l10n_it_asset_management/wizard/account_move_manage_asset_view.xml b/l10n_it_asset_management/wizard/account_move_manage_asset_view.xml index 732600f9e4ee..b75792f89b58 100644 --- a/l10n_it_asset_management/wizard/account_move_manage_asset_view.xml +++ b/l10n_it_asset_management/wizard/account_move_manage_asset_view.xml @@ -125,6 +125,28 @@ /> + + + + + + + +