diff --git a/account_move_update_analytic/__init__.py b/account_move_update_analytic/__init__.py index 5cb1c49143..aee8895e7a 100644 --- a/account_move_update_analytic/__init__.py +++ b/account_move_update_analytic/__init__.py @@ -1 +1,2 @@ +from . import models from . import wizards diff --git a/account_move_update_analytic/models/account_move_line.py b/account_move_update_analytic/models/account_move_line.py new file mode 100644 index 0000000000..db4b8fd0ac --- /dev/null +++ b/account_move_update_analytic/models/account_move_line.py @@ -0,0 +1,31 @@ +# Copyright 2024 Hunki Enterprises BV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models + +force_state_sentinel = object() + + +class AccountMoveLine(models.Model): + _inherit = "account.move.line" + + def _compute_all_tax(self): + """ + super() doesn't write the analytic distribution when the move is posted. + For our purposes, we need that, so we manipulate the cache for the move to + look like it is in draft state when we're called by the update wizard + """ + + self.move_id.read(["state"]) + cache = self.env.cache._data[self.move_id._fields["state"]] + + if self.env.context.get("account_move_update_analytic") == force_state_sentinel: + cache[self.move_id.id] = "draft" + + result = super()._compute_all_tax() + + if self.env.context.get("account_move_update_analytic") == force_state_sentinel: + if self.move_id.id in cache: + del cache[self.move_id.id] + + return result diff --git a/account_move_update_analytic/tests/__init__.py b/account_move_update_analytic/tests/__init__.py new file mode 100644 index 0000000000..c63d7b5a3d --- /dev/null +++ b/account_move_update_analytic/tests/__init__.py @@ -0,0 +1 @@ +from . import test_account_move_update_analytic diff --git a/account_move_update_analytic/tests/test_account_move_update_analytic.py b/account_move_update_analytic/tests/test_account_move_update_analytic.py new file mode 100644 index 0000000000..a3863018fd --- /dev/null +++ b/account_move_update_analytic/tests/test_account_move_update_analytic.py @@ -0,0 +1,101 @@ +from odoo.fields import Command +from odoo.tests.common import TransactionCase, tagged + + +@tagged("-at_install", "post_install") +class TestAccountMoveUpdateAnalytic(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.line = cls.env["account.move.line"].search( + [ + ("product_id", "!=", False), + ("move_id.move_type", "=", "in_invoice"), + ("move_id.state", "=", "posted"), + ("analytic_distribution", "=", False), + ], + limit=1, + ) + cls.plan = cls.env.ref("analytic.analytic_plan_projects") + cls.account1 = cls.env["account.analytic.account"].search( + [("plan_id", "=", cls.plan.id)], limit=1 + ) + cls.account2 = cls.env["account.analytic.account"].search( + [("plan_id", "=", cls.plan.id), ("id", "!=", cls.account1.id)], limit=1 + ) + + def test_analytic_line_created(self): + """ + Test that changing analytic distribution recreates lines + """ + wizard = ( + self.env["account.move.update.analytic.wizard"] + .with_context(active_id=self.line.id) + .create( + { + "analytic_distribution": {str(self.account1.id): 100}, + } + ) + ) + wizard.update_analytic_lines() + self.assertEqual(self.line.analytic_line_ids.account_id, self.account1) + wizard = ( + self.env["account.move.update.analytic.wizard"] + .with_context(active_id=self.line.id) + .create( + { + "analytic_distribution": {str(self.account2.id): 100}, + } + ) + ) + wizard.update_analytic_lines() + self.assertEqual(self.line.analytic_line_ids.account_id, self.account2) + + def test_tax_distribution_added(self): + """ + Test that changing analytic distribution on a line with a tax with analytic + enabled changes the tax' analytic lines too, and splits tax lines if multiple + lines have the same tax but a different analytic distribution + """ + tax = self.env["account.tax"].search( + [("type_tax_use", "=", "purchase")], limit=1 + ) + tax.analytic = True + move = self.line.move_id.copy({"invoice_date": self.line.move_id.invoice_date}) + move.invoice_line_ids[1:].unlink() + line1 = move.invoice_line_ids + line1.write( + { + "tax_ids": [Command.set(tax.ids)], + "analytic_distribution": {self.account1.id: 100}, + } + ) + line2 = line1.copy({}) + line2.analytic_distribution = {self.account1.id: 100} + move.action_post() + self.assertEqual(line1.analytic_line_ids.account_id, self.account1) + self.assertEqual( + move.line_ids.filtered("tax_line_id").analytic_line_ids.account_id, + self.account1, + ) + wizard = ( + self.env["account.move.update.analytic.wizard"] + .with_context(active_id=line1.id) + .create( + { + "analytic_distribution": {self.account2.id: 100}, + } + ) + ) + wizard.update_analytic_lines() + self.assertEqual(line1.analytic_line_ids.account_id, self.account2) + self.assertEqual(line2.analytic_line_ids.account_id, self.account1) + tax_lines = move.line_ids.filtered(lambda x: x.tax_line_id == tax) + self.assertEqual(len(tax_lines), 2) + self.assertEqual( + len(list(filter(None, tax_lines.mapped("analytic_distribution")))), 2 + ) + self.assertItemsEqual( + tax_lines.analytic_line_ids.account_id, + self.account1 + self.account2, + ) diff --git a/account_move_update_analytic/wizards/account_move_update_analytic.py b/account_move_update_analytic/wizards/account_move_update_analytic.py index db3c7355c3..2308906cb3 100644 --- a/account_move_update_analytic/wizards/account_move_update_analytic.py +++ b/account_move_update_analytic/wizards/account_move_update_analytic.py @@ -4,6 +4,8 @@ from odoo import api, fields, models +from ..models.account_move_line import force_state_sentinel + class AccountMoveUpdateAnalytic(models.TransientModel): _name = "account.move.update.analytic.wizard" @@ -40,4 +42,13 @@ def default_get(self, fields): def update_analytic_lines(self): self.ensure_one() - self.line_id.analytic_distribution = self.analytic_distribution + + # force recreating lines with analytic taxes + taxes_with_analytic = self.line_id.tax_ids.filtered("analytic") + self.line_id.move_id.line_ids.filtered( + lambda x: x.tax_line_id in taxes_with_analytic + ).with_context(dynamic_unlink=True, force_delete=True).unlink() + + self.line_id.with_context( + account_move_update_analytic=force_state_sentinel + ).analytic_distribution = self.analytic_distribution