diff --git a/l10n_it_asset_management/README.rst b/l10n_it_asset_management/README.rst index fd096a46402d..0cc435f94d6d 100644 --- a/l10n_it_asset_management/README.rst +++ b/l10n_it_asset_management/README.rst @@ -50,6 +50,11 @@ Depreciations can be generated by using the related wizard found in Assets -> Assets Management -> Generate Depreciations, or by triggering the same wizard from a single asset form view. +When an asset is returned, it is possible to recharge its purchase and +fund amounts choosing "Partial Recharge" in the "Link to Asset" wizard +of the credit note. The wizard will allow to link to the credit note +only the assets of the refunded invoice. + **Italiano** È possibile creare e gestire cespiti dalla sezione contabilità di Odoo. @@ -63,6 +68,11 @@ contabili. Gli ammortamenti possono essere generati utilizzando l'apposito wizard in Cespiti -> Gestione Cestpiti -> Genera Ammortamenti, o aprendo quello stesso wizard dalla scheda del cespite. +Quando un cespite viene restituito, è possibile ricaricare gli importi +di acquisto e di fondo scegliendo "Ricarica parziale" nella procedura +"Collega a cespite" della nota di credito. La procedura consentirà di +collegare alla nota di credito solo i cespiti della fattura rimborsata. + Bug Tracker =========== @@ -99,8 +109,7 @@ Contributors - Nextev Srl Base icon made by `surang `__ -from -[`www.flaticon.com](https://www.flaticon.com/) `__. +from `www.flaticon.com `__. Maintainers ----------- 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.py b/l10n_it_asset_management/models/asset_depreciation.py index 994d50671c25..ea2f33ef241d 100644 --- a/l10n_it_asset_management/models/asset_depreciation.py +++ b/l10n_it_asset_management/models/asset_depreciation.py @@ -252,7 +252,7 @@ def _compute_last_depreciation_date(self): for dep in self: dep_lines = dep.line_ids.filtered( lambda line: line.move_type == "depreciated" - and not line.partial_dismissal + and not (line.partial_dismissal or line.partial_recharge) ) if dep_lines: dep.last_depreciation_date = max(dep_lines.mapped("date")) diff --git a/l10n_it_asset_management/models/asset_depreciation_line.py b/l10n_it_asset_management/models/asset_depreciation_line.py index 40296df2b197..500787877b12 100644 --- a/l10n_it_asset_management/models/asset_depreciation_line.py +++ b/l10n_it_asset_management/models/asset_depreciation_line.py @@ -100,6 +100,7 @@ class AssetDepreciationLine(models.Model): ) partial_dismissal = fields.Boolean() + partial_recharge = fields.Boolean() percentage = fields.Float(string="%") @@ -265,9 +266,8 @@ def get_update_move_types(self): def is_depreciation_nr_required(self): """Defines if a line requires to be numbered""" self.ensure_one() - return ( - self.move_type in self.get_numbered_move_types() - and not self.partial_dismissal + return self.move_type in self.get_numbered_move_types() and not ( + self.partial_dismissal or self.partial_recharge ) def make_name(self): @@ -394,15 +394,22 @@ def get_depreciated_account_move_line_vals(self): self.ensure_one() # Asset depreciation - if not self.partial_dismissal: + if not (self.partial_dismissal or self.partial_recharge): credit_account_id = self.asset_id.category_id.fund_account_id.id debit_account_id = self.asset_id.category_id.depreciation_account_id.id - # Asset partial dismissal else: + # Asset partial dismissal debit_account_id = self.asset_id.category_id.fund_account_id.id credit_account_id = self.asset_id.category_id.asset_account_id.id + # Asset partial recharge + if self.partial_recharge: + credit_account_id, debit_account_id = ( + debit_account_id, + credit_account_id, + ) + amt = abs(self.amount) credit_line_vals = { "account_id": credit_account_id, @@ -495,3 +502,6 @@ def post_partial_dismiss_asset(self): ) if to_create_move: to_create_move.generate_account_move() + + def post_partial_recharge_asset(self): + return self.post_partial_dismiss_asset() diff --git a/l10n_it_asset_management/readme/USAGE.md b/l10n_it_asset_management/readme/USAGE.md index 75a337b11f92..809dc498e223 100644 --- a/l10n_it_asset_management/readme/USAGE.md +++ b/l10n_it_asset_management/readme/USAGE.md @@ -10,6 +10,9 @@ Depreciations can be generated by using the related wizard found in Assets -\> Assets Management -\> Generate Depreciations, or by triggering the same wizard from a single asset form view. +When an asset is returned, it is possible to recharge its purchase and fund amounts choosing "Partial Recharge" in the "Link to Asset" wizard of the credit note. +The wizard will allow to link to the credit note only the assets of the refunded invoice. + **Italiano** È possibile creare e gestire cespiti dalla sezione contabilità di Odoo. @@ -22,3 +25,6 @@ I cespiti possono essere creati manualmente o da fatture e registrazioni contabili. Gli ammortamenti possono essere generati utilizzando l'apposito wizard in Cespiti -\> Gestione Cestpiti -\> Genera Ammortamenti, o aprendo quello stesso wizard dalla scheda del cespite. + +Quando un cespite viene restituito, è possibile ricaricare gli importi di acquisto e di fondo scegliendo "Ricarica parziale" nella procedura "Collega a cespite" della nota di credito. +La procedura consentirà di collegare alla nota di credito solo i cespiti della fattura rimborsata. diff --git a/l10n_it_asset_management/report/asset_journal.py b/l10n_it_asset_management/report/asset_journal.py index f00a7ece325a..fa9abe8fbfd0 100644 --- a/l10n_it_asset_management/report/asset_journal.py +++ b/l10n_it_asset_management/report/asset_journal.py @@ -728,7 +728,7 @@ def get_report_dep_line_year_data(self): line.amount for line in self.dep_line_ids.filtered( lambda line: line.move_type == "depreciated" - and not line.partial_dismissal + and not (line.partial_dismissal or line.partial_recharge) ) ] ) @@ -737,7 +737,7 @@ def get_report_dep_line_year_data(self): line.amount for line in self.dep_line_ids.filtered( lambda line: line.move_type == "depreciated" - and line.partial_dismissal + and (line.partial_dismissal or line.partial_recharge) ) ] ) diff --git a/l10n_it_asset_management/report/asset_previsional.py b/l10n_it_asset_management/report/asset_previsional.py index efe2d0e9aabf..a2cb7b52f564 100644 --- a/l10n_it_asset_management/report/asset_previsional.py +++ b/l10n_it_asset_management/report/asset_previsional.py @@ -245,7 +245,9 @@ def generate_structure(self): if fyear.date_to >= dep.date_start: prev = not lines or not any( line.move_type == "depreciated" - and not line.partial_dismissal + and not ( + line.partial_dismissal or line.partial_recharge + ) for line in lines ) sequence += 1 @@ -797,7 +799,7 @@ def get_report_dep_line_year_data(self): line.amount for line in self.dep_line_ids.filtered( lambda line: line.move_type == "depreciated" - and not line.partial_dismissal + and not (line.partial_dismissal or line.partial_recharge) ) ] ) @@ -806,7 +808,7 @@ def get_report_dep_line_year_data(self): line.amount for line in self.dep_line_ids.filtered( lambda line: line.move_type == "depreciated" - and line.partial_dismissal + and (line.partial_dismissal or line.partial_recharge) ) ] ) diff --git a/l10n_it_asset_management/static/description/index.html b/l10n_it_asset_management/static/description/index.html index e8d20b8bfad4..bd6937de2970 100644 --- a/l10n_it_asset_management/static/description/index.html +++ b/l10n_it_asset_management/static/description/index.html @@ -394,6 +394,10 @@

Usage

Depreciations can be generated by using the related wizard found in Assets -> Assets Management -> Generate Depreciations, or by triggering the same wizard from a single asset form view.

+

When an asset is returned, it is possible to recharge its purchase and +fund amounts choosing “Partial Recharge” in the “Link to Asset” wizard +of the credit note. The wizard will allow to link to the credit note +only the assets of the refunded invoice.

Italiano

È possibile creare e gestire cespiti dalla sezione contabilità di Odoo.

La configurazione dei cespiti dev’essere fatta andando in Cespiti -> @@ -403,6 +407,10 @@

Usage

contabili. Gli ammortamenti possono essere generati utilizzando l’apposito wizard in Cespiti -> Gestione Cestpiti -> Genera Ammortamenti, o aprendo quello stesso wizard dalla scheda del cespite.

+

Quando un cespite viene restituito, è possibile ricaricare gli importi +di acquisto e di fondo scegliendo “Ricarica parziale” nella procedura +“Collega a cespite” della nota di credito. La procedura consentirà di +collegare alla nota di credito solo i cespiti della fattura rimborsata.

Bug Tracker

@@ -438,8 +446,7 @@

Contributors

  • Nextev Srl <odoo@nextev.it>
  • Base icon made by surang -from -[www.flaticon.com](https://www.flaticon.com/).

    +from www.flaticon.com.

    Maintainers

    diff --git a/l10n_it_asset_management/tests/common.py b/l10n_it_asset_management/tests/common.py index 54afe33feacf..14f874176ac6 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,31 @@ 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 _get_move_asset_wizard(self, move, link_management_type, wiz_values=None): + """Get the wizard that links `move` to an asset + 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() + 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 - return wizard.link_asset() + def _link_asset_move(self, move, link_management_type, wiz_values=None): + """Link `move` to an asset with mode `link_management_type`. + `wiz_values` are values to be set in the wizard. + """ + wiz = self._get_move_asset_wizard( + move, link_management_type, wiz_values=wiz_values + ) + 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..fce9e2342223 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,132 @@ 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, + }, + ) + civ_depreciation_lines = civ_depreciation.line_ids + self.assertRecordValues( + civ_depreciation_lines.sorted("move_type"), + [ + { + "move_type": "depreciated", + "amount": -1000.0, + }, + { + "move_type": "gain", + "amount": 8000.0, + }, + { + "move_type": "out", + "amount": 1000.0, + }, + ], + ) + civ_depreciation_move_lines = civ_depreciation_lines.filtered( + lambda cdl: cdl.move_type == "depreciated" + ).move_id.line_ids + self.assertRecordValues( + civ_depreciation_move_lines.sorted("balance"), + [ + { + "account_id": asset.category_id.asset_account_id.id, + "balance": -1000, + }, + { + "account_id": asset.category_id.fund_account_id.id, + "balance": 1000, + }, + ], + ) + 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 + wizard = self._get_move_asset_wizard( + sale_refund, + "partial_recharge", + wiz_values={ + "recharge_purchase_amount": recharge_purchase_amount, + "recharge_fund_amount": recharge_fund_amount, + }, + ) + wizard.link_asset() + self.assertEqual(wizard.asset_id, asset) + self.assertEqual(wizard.allowed_asset_ids, asset) + civ_depreciation_lines = civ_depreciation.line_ids - civ_depreciation_lines + self.assertRecordValues( + civ_depreciation_lines.sorted("move_type"), + [ + { + "move_type": "depreciated", + "amount": 1000.0, + }, + { + "move_type": "in", + "amount": 1000.0, + }, + { + "move_type": "loss", + "amount": 8000.0, + }, + ], + ) + civ_depreciation_move_lines = civ_depreciation_lines.filtered( + lambda cdl: cdl.move_type == "depreciated" + ).move_id.line_ids + self.assertRecordValues( + civ_depreciation_move_lines.sorted("balance"), + [ + { + "account_id": asset.category_id.fund_account_id.id, + "balance": -1000, + }, + { + "account_id": asset.category_id.asset_account_id.id, + "balance": 1000, + }, + ], + ) + self.assertEqual( + civ_depreciation.amount_depreciable_updated, + purchase_amount, + ) diff --git a/l10n_it_asset_management/views/account_move.xml b/l10n_it_asset_management/views/account_move.xml index 0d68c8cf8e5e..7703007bfaeb 100644 --- a/l10n_it_asset_management/views/account_move.xml +++ b/l10n_it_asset_management/views/account_move.xml @@ -100,9 +100,19 @@ /> + diff --git a/l10n_it_asset_management/views/asset_depreciation.xml b/l10n_it_asset_management/views/asset_depreciation.xml index 3193b56bd33a..26a6549f07d9 100644 --- a/l10n_it_asset_management/views/asset_depreciation.xml +++ b/l10n_it_asset_management/views/asset_depreciation.xml @@ -99,10 +99,22 @@ /> + 0 else "loss", + "name": name, + "partial_recharge": True, + } + 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_recharge_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..ce5e3242e470 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 @@ -55,6 +55,7 @@ placeholder="Asset Name" attrs="{'invisible': [('management_type', '!=', 'create')], 'required': [('management_type', '=', 'create')]}" /> + + + + + + + + +