diff --git a/account_move_line_tax_single/__init__.py b/account_move_line_tax_single/__init__.py
new file mode 100644
index 000000000000..c0d9f3d12f7d
--- /dev/null
+++ b/account_move_line_tax_single/__init__.py
@@ -0,0 +1,4 @@
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+from . import models
+from .hooks import post_init_hook
diff --git a/account_move_line_tax_single/__manifest__.py b/account_move_line_tax_single/__manifest__.py
new file mode 100644
index 000000000000..b08a470d6f3a
--- /dev/null
+++ b/account_move_line_tax_single/__manifest__.py
@@ -0,0 +1,21 @@
+# Copyright 2020 CorporateHub (https://corporatehub.eu)
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+{
+ "name": "Accounting: Account Move Line Tax (single)",
+ "summary": "Allows only one tax to be applied to an account move line.",
+ "version": "12.0.1.0.0",
+ "category": "Accounting",
+ "website": "https://github.com/OCA/account-financial-tools",
+ "author": "CorporateHub, Odoo Community Association (OCA)",
+ "maintainers": ["alexey-pelykh"],
+ "license": "AGPL-3",
+ "application": False,
+ "installable": True,
+ "post_init_hook": "post_init_hook",
+ "depends": [
+ "account",
+ ],
+ "data": [
+ "views/account_move_line.xml",
+ ],
+}
diff --git a/account_move_line_tax_single/hooks.py b/account_move_line_tax_single/hooks.py
new file mode 100644
index 000000000000..658ec89006a4
--- /dev/null
+++ b/account_move_line_tax_single/hooks.py
@@ -0,0 +1,11 @@
+# Copyright 2020 CorporateHub (https://corporatehub.eu)
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+from odoo import api, SUPERUSER_ID
+
+
+def post_init_hook(cr, registry):
+ env = api.Environment(cr, SUPERUSER_ID, {})
+ statement_lines = env["account.move.line"].search([])
+ statement_lines._compute_tax_id()
+ statement_lines._inverse_tax_id()
diff --git a/account_move_line_tax_single/models/__init__.py b/account_move_line_tax_single/models/__init__.py
new file mode 100644
index 000000000000..bc7621f756ac
--- /dev/null
+++ b/account_move_line_tax_single/models/__init__.py
@@ -0,0 +1,3 @@
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+from . import account_move_line
diff --git a/account_move_line_tax_single/models/account_move_line.py b/account_move_line_tax_single/models/account_move_line.py
new file mode 100644
index 000000000000..fb3c4695cbfb
--- /dev/null
+++ b/account_move_line_tax_single/models/account_move_line.py
@@ -0,0 +1,50 @@
+# Copyright 2020 CorporateHub (https://corporatehub.eu)
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+import logging
+
+from odoo import api, fields, models, _
+from odoo.exceptions import ValidationError
+
+_logger = logging.getLogger(__name__)
+
+
+class AccountMoveLine(models.Model):
+ _inherit = "account.move.line"
+
+ tax_id = fields.Many2one(
+ string="Tax Applied",
+ comodel_name="account.tax",
+ domain=["|", ("active", "=", False), ("active", "=", True)],
+ compute="_compute_tax_id",
+ inverse="_inverse_tax_id",
+ store=True,
+ )
+
+ @api.multi
+ @api.depends("tax_ids")
+ def _compute_tax_id(self):
+ for line in self:
+ line.tax_id = line.tax_ids[:1]
+
+ @api.multi
+ def _inverse_tax_id(self):
+ for line in self:
+ if len(line.tax_ids) > 1:
+ _logger.warning(
+ _(
+ "Account Move Line #%s has more than one tax applied!"
+ ) % (
+ line.id,
+ )
+ )
+ line.tax_ids |= line.tax_id
+ continue
+ line.tax_ids = line.tax_id
+
+ @api.constrains("tax_ids")
+ def _validate_single_tax(self):
+ if self.env.context.get("skip_validate_single_tax"):
+ return
+ if self.filtered(lambda line: len(line.tax_ids) > 1):
+ raise ValidationError(_("Only single tax per line is allowed."))
diff --git a/account_move_line_tax_single/readme/CONTRIBUTORS.rst b/account_move_line_tax_single/readme/CONTRIBUTORS.rst
new file mode 100644
index 000000000000..724bc1d03a2b
--- /dev/null
+++ b/account_move_line_tax_single/readme/CONTRIBUTORS.rst
@@ -0,0 +1,3 @@
+* `CorporateHub `__
+
+ * Alexey Pelykh
diff --git a/account_move_line_tax_single/readme/DESCRIPTION.rst b/account_move_line_tax_single/readme/DESCRIPTION.rst
new file mode 100644
index 000000000000..93c07d272a0b
--- /dev/null
+++ b/account_move_line_tax_single/readme/DESCRIPTION.rst
@@ -0,0 +1,2 @@
+This module alters the functionality of Accounting to use only single tax per
+Account Move Line.
diff --git a/account_move_line_tax_single/tests/__init__.py b/account_move_line_tax_single/tests/__init__.py
new file mode 100644
index 000000000000..8c0c3d828f4f
--- /dev/null
+++ b/account_move_line_tax_single/tests/__init__.py
@@ -0,0 +1,3 @@
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+from . import test_account_move_line
diff --git a/account_move_line_tax_single/tests/test_account_move_line.py b/account_move_line_tax_single/tests/test_account_move_line.py
new file mode 100644
index 000000000000..004ebba717f5
--- /dev/null
+++ b/account_move_line_tax_single/tests/test_account_move_line.py
@@ -0,0 +1,108 @@
+# Copyright 2020 CorporateHub (https://corporatehub.eu)
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+from odoo.tests.common import TransactionCase
+from odoo.exceptions import ValidationError
+
+
+class TestAccountMoveLine(TransactionCase):
+
+ def setUp(self):
+ super().setUp()
+
+ self.Account = self.env["account.account"]
+ self.Move = self.env["account.move"]
+ self.Journal = self.env["account.journal"]
+ self.Tax = self.env["account.tax"]
+
+ def constraint(self):
+ account = self.Account.create({
+ "code": "1",
+ "name": "Account",
+ })
+ journal = self.Journal.create({
+ "name": "Journal",
+ "type": "sale",
+ "code": "JOURNAL",
+ })
+ tax_1 = self.Tax.create({
+ "name": "Tax 1",
+ "amount": 15.0,
+ "amount_type": "percent",
+ "type_tax_use": "sale",
+ })
+ tax_2 = self.Tax.create({
+ "name": "Tax 2",
+ "amount": 15.0,
+ "amount_type": "percent",
+ "type_tax_use": "sale",
+ })
+ with self.assertRaises(ValidationError):
+ self.Move.create({
+ "journal_id": journal.id,
+ "name": "Move",
+ "date": "2020-11-01",
+ "line_ids": [
+ (0, 0, {
+ "name": "Move Line",
+ "debit": 0.0,
+ "credit": 1000.0,
+ "account_id": account.id,
+ "tax_ids": [(6, 0, [tax_1, tax_2])],
+ }),
+ ],
+ })
+
+ def nondestructive(self):
+ account = self.Account.create({
+ "code": "1",
+ "name": "Account",
+ })
+ journal = self.Journal.create({
+ "name": "Journal",
+ "type": "sale",
+ "code": "JOURNAL",
+ })
+ tax_1 = self.Tax.create({
+ "name": "Tax 1",
+ "amount": 15.0,
+ "amount_type": "percent",
+ "type_tax_use": "sale",
+ })
+ tax_2 = self.Tax.create({
+ "name": "Tax 2",
+ "amount": 15.0,
+ "amount_type": "percent",
+ "type_tax_use": "sale",
+ })
+ tax_3 = self.Tax.create({
+ "name": "Tax 3",
+ "amount": 15.0,
+ "amount_type": "percent",
+ "type_tax_use": "sale",
+ })
+ move = self.Move.with_context(skip_validate_single_tax=True).create({
+ "journal_id": journal.id,
+ "name": "Move",
+ "date": "2020-11-01",
+ "line_ids": [
+ (0, 0, {
+ "name": "Move Line",
+ "debit": 0.0,
+ "credit": 1000.0,
+ "account_id": account.id,
+ "tax_ids": [(6, 0, [tax_1, tax_2])],
+ }),
+ ],
+ })
+
+ move.line_ids.tax_id = tax_2
+ self.assertEqual(move.line_ids.tax_ids, (tax_1 | tax_2))
+
+ with self.assertLogs() as logger_context:
+ move.line_ids.tax_id = tax_3
+ self.assertRegex(
+ logger_context.output[0],
+ r"Account Move Line #\d+ has more than one tax applied!"
+ )
+ self.assertEqual(move.line_ids.tax_ids, (tax_1 | tax_2 | tax_3))
diff --git a/account_move_line_tax_single/views/account_move_line.xml b/account_move_line_tax_single/views/account_move_line.xml
new file mode 100644
index 000000000000..82f012147bd6
--- /dev/null
+++ b/account_move_line_tax_single/views/account_move_line.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+ account.move.line.form
+ account.move.line
+
+
+
+
+
+
+ base.group_no_one
+
+
+
+
+
+ account.move.line.filter
+ account.move.line
+
+
+
+
+
+
+
+
+
+
+
+