diff --git a/account_ecotax/models/account_ecotax_classification.py b/account_ecotax/models/account_ecotax_classification.py index 5e75b7b73..bad631308 100644 --- a/account_ecotax/models/account_ecotax_classification.py +++ b/account_ecotax/models/account_ecotax_classification.py @@ -82,6 +82,12 @@ class AccountEcotaxClassification(models.Model): ) intrastat_code = fields.Char() scale_code = fields.Char() + country_ids = fields.Many2many( + "res.country", + string="Countries", + help="Ecotax will be applied when delivered in the listed countries (or all " + "countries if empty)", + ) @api.depends("ecotax_type") def _compute_ecotax_vals(self): diff --git a/account_ecotax/models/account_move_line.py b/account_ecotax/models/account_move_line.py index f516a2884..c14832322 100644 --- a/account_ecotax/models/account_move_line.py +++ b/account_ecotax/models/account_move_line.py @@ -46,6 +46,12 @@ def _compute_ecotax(self): def _get_new_vals_list(self): self.ensure_one() + if not self.product_id: + return [] + country = ( + self.move_id.partner_shipping_id.country_id + or self.move_id.partner_id.country_id + ) new_vals_list = [ Command.create( { @@ -53,11 +59,13 @@ def _get_new_vals_list(self): "force_amount_unit": ecotaxline_prod.force_amount, } ) - for ecotaxline_prod in self.product_id.all_ecotax_line_product_ids + for ecotaxline_prod in self.product_id._get_country_eligible_classification( + country + ) ] return new_vals_list - @api.depends("product_id") + @api.depends("product_id", "move_id.partner_id", "move_id.partner_shipping_id") def _compute_ecotax_line_ids(self): """Unlink and recreate ecotax_lines when modifying the product_id.""" for line in self: diff --git a/account_ecotax/models/ecotax_line_product.py b/account_ecotax/models/ecotax_line_product.py index 3c5cc9cc4..4a7026509 100644 --- a/account_ecotax/models/ecotax_line_product.py +++ b/account_ecotax/models/ecotax_line_product.py @@ -32,6 +32,11 @@ class EcotaxLineProduct(models.Model): help="Ecotax Amount computed form Classification or forced ecotax amount", store=True, ) + country_ids = fields.Many2many( + "res.country", + string="Countries", + related="classification_id.country_ids", + ) display_name = fields.Char(compute="_compute_display_name") @api.depends("classification_id", "amount") diff --git a/account_ecotax/models/product_product.py b/account_ecotax/models/product_product.py index fa108141e..c866c7ccd 100644 --- a/account_ecotax/models/product_product.py +++ b/account_ecotax/models/product_product.py @@ -75,16 +75,34 @@ def _search_all_ecotax_line_product_ids(self, operator, operand): ) def _compute_product_ecotax(self): for product in self: - amount_ecotax = 0.0 - weight_based_ecotax = 0.0 - fixed_ecotax = 0.0 - for ecotaxline_prod in product.all_ecotax_line_product_ids: - ecotax_cls = ecotaxline_prod.classification_id - if ecotax_cls.ecotax_type == "weight_based": - weight_based_ecotax += ecotaxline_prod.amount - else: - fixed_ecotax += ecotaxline_prod.amount - amount_ecotax += ecotaxline_prod.amount + ( + fixed_ecotax, + weight_based_ecotax, + amount_ecotax, + ) = product._get_ecotax_amounts_from_classification( + product.all_ecotax_line_product_ids + ) product.fixed_ecotax = fixed_ecotax product.weight_based_ecotax = weight_based_ecotax product.ecotax_amount = amount_ecotax + + def _get_ecotax_amounts_from_classification(self, classifications): + self.ensure_one() + amount_ecotax = 0.0 + weight_based_ecotax = 0.0 + fixed_ecotax = 0.0 + for ecotaxline_prod in classifications: + ecotax_cls = ecotaxline_prod.classification_id + if ecotax_cls.ecotax_type == "weight_based": + weight_based_ecotax += ecotaxline_prod.amount + else: + fixed_ecotax += ecotaxline_prod.amount + amount_ecotax += ecotaxline_prod.amount + return fixed_ecotax, weight_based_ecotax, amount_ecotax + + def _get_country_eligible_classification(self, country): + self and self.ensure_one() + return self.all_ecotax_line_product_ids.filtered( + lambda line: not line.country_ids + or (country and country in line.country_ids) + ) diff --git a/account_ecotax/tests/test_ecotax.py b/account_ecotax/tests/test_ecotax.py index 7f5d62dbe..d56b7c46f 100644 --- a/account_ecotax/tests/test_ecotax.py +++ b/account_ecotax/tests/test_ecotax.py @@ -405,6 +405,33 @@ def _test_05_product_variants(self): variant_2.product_tmpl_id.ecotax_line_product_ids, ) + def _test_06_ecotax_by_country(self): + """ + Test default ecotax by country + """ + self.invoice_partner.write({"country_id": self.env.ref("base.fr").id}) + invoice = self._make_invoice(products=self._make_product(self.ecotax_fixed)) + # no country ecotax classification is set + self.assertEqual( + invoice.invoice_line_ids.ecotax_line_ids.classification_id, + self.ecotax_fixed, + ) + # in case of wrong country, the ecotax is not set + self.ecotax_fixed.write( + {"country_ids": [Command.link(self.env.ref("base.de").id)]} + ) + invoice.write({"partner_id": self.invoice_partner.id}) + self.assertFalse(invoice.invoice_line_ids.ecotax_line_ids.classification_id) + # in case the same country is set, the ecotax is set as well + self.ecotax_fixed.write( + {"country_ids": [Command.link(self.env.ref("base.fr").id)]} + ) + invoice.write({"partner_id": self.invoice_partner.id}) + self.assertEqual( + invoice.invoice_line_ids.ecotax_line_ids.classification_id, + self.ecotax_fixed, + ) + class TestInvoiceEcotax(TestInvoiceEcotaxCommon): def test_01_default_fixed_ecotax(self): @@ -421,3 +448,6 @@ def test_04_mixed_ecotax(self): def test_05_product_variants(self): self._test_05_product_variants() + + def test_06_ecotax_by_country(self): + self._test_06_ecotax_by_country() diff --git a/account_ecotax/views/account_ecotax_classification_view.xml b/account_ecotax/views/account_ecotax_classification_view.xml index 21fb08175..7f8270c5c 100644 --- a/account_ecotax/views/account_ecotax_classification_view.xml +++ b/account_ecotax/views/account_ecotax_classification_view.xml @@ -22,6 +22,7 @@ /> + @@ -58,6 +59,7 @@ + + diff --git a/account_ecotax/views/product_view.xml b/account_ecotax/views/product_view.xml index f936b971e..3b1b07687 100644 --- a/account_ecotax/views/product_view.xml +++ b/account_ecotax/views/product_view.xml @@ -26,6 +26,7 @@ + @@ -43,6 +44,7 @@ + @@ -73,6 +75,7 @@ + diff --git a/account_ecotax_sale/models/sale_order_line.py b/account_ecotax_sale/models/sale_order_line.py index 5b512907f..7c2c2fa99 100644 --- a/account_ecotax_sale/models/sale_order_line.py +++ b/account_ecotax_sale/models/sale_order_line.py @@ -47,6 +47,12 @@ def _compute_ecotax(self): def _get_new_vals_list(self): self.ensure_one() + if not self.product_id: + return [] + country = ( + self.order_id.partner_shipping_id.country_id + or self.order_id.partner_id.country_id + ) new_vals_list = [ Command.create( { @@ -54,11 +60,13 @@ def _get_new_vals_list(self): "force_amount_unit": ecotaxline_prod.force_amount, } ) - for ecotaxline_prod in self.product_id.all_ecotax_line_product_ids + for ecotaxline_prod in self.product_id._get_country_eligible_classification( + country + ) ] return new_vals_list - @api.depends("product_id") + @api.depends("product_id", "order_id.partner_id", "order_id.partner_shipping_id") def _compute_ecotax_line_ids(self): """Unlink and recreate ecotax_lines when modifying the product_id.""" for line in self: diff --git a/account_ecotax_sale/tests/test_sale_ecotax.py b/account_ecotax_sale/tests/test_sale_ecotax.py index 7fd8a648f..494baa1ea 100644 --- a/account_ecotax_sale/tests/test_sale_ecotax.py +++ b/account_ecotax_sale/tests/test_sale_ecotax.py @@ -2,6 +2,7 @@ # @author Mourad EL HADJ MIMOUNE # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo import Command from odoo.tests.common import Form from odoo.addons.account_ecotax.tests.test_ecotax import TestInvoiceEcotaxCommon @@ -106,6 +107,34 @@ def _test_02_classification_ecotax(self): self.assertEqual(self.sale.amount_total, 1000.0) self.assertEqual(self.sale.amount_ecotax, 47.0) + def _test_03_ecotax_by_country(self): + """ + Test default ecotax by country + """ + partner12 = self.env.ref("base.res_partner_12") + partner12.write({"country_id": self.env.ref("base.fr").id}) + sale = self.create_sale_partner( + partner_id=partner12, products_and_qty=[(self.product_a, 1.0)] + ) + # no country ecotax classification is set, ecotax is taken by default + self.assertEqual( + sale.order_line.ecotax_line_ids.classification_id, self.ecotax_fixed + ) + # in case of wrong country, the ecotax is not set + self.ecotax_fixed.write( + {"country_ids": [Command.link(self.env.ref("base.de").id)]} + ) + sale.write({"partner_id": partner12.id}) + self.assertFalse(sale.order_line.ecotax_line_ids.classification_id) + # in case the same country is set, the ecotax is set as well + self.ecotax_fixed.write( + {"country_ids": [Command.link(self.env.ref("base.fr").id)]} + ) + sale.write({"partner_id": sale.partner_id.id}) + self.assertEqual( + sale.order_line.ecotax_line_ids.classification_id, self.ecotax_fixed + ) + class TestsaleEcotax(TestsaleEcotaxCommon): def test_01_classification_weight_based_ecotax(self): @@ -113,3 +142,6 @@ def test_01_classification_weight_based_ecotax(self): def test_02_classification_ecotax(self): self._test_02_classification_ecotax() + + def test_03_ecotax_by_country(self): + self._test_03_ecotax_by_country() diff --git a/account_ecotax_sale_tax/models/sale_order_line.py b/account_ecotax_sale_tax/models/sale_order_line.py index 78606fb2d..2f1fb2db8 100644 --- a/account_ecotax_sale_tax/models/sale_order_line.py +++ b/account_ecotax_sale_tax/models/sale_order_line.py @@ -54,7 +54,12 @@ def _get_new_vals_list(self): def _compute_ecotax_line_ids(self): return super()._compute_ecotax_line_ids() - @api.depends("product_id", "company_id") + @api.depends( + "product_id", + "company_id", + "order_id.partner_id", + "order_id.partner_shipping_id", + ) def _compute_tax_id(self): res = super()._compute_tax_id() for line in self: @@ -63,10 +68,15 @@ def _compute_tax_id(self): def _get_computed_ecotaxes(self): self.ensure_one() - sale_ecotaxes = self.product_id.all_ecotax_line_product_ids.mapped( - "classification_id" - ).mapped("sale_ecotax_ids") - ecotax_ids = sale_ecotaxes.filtered( + country = ( + self.order_id.partner_shipping_id.country_id + or self.order_id.partner_id.country_id + ) + eligible_classifications = self.product_id._get_country_eligible_classification( + country + ) + sale_ecotaxs = eligible_classifications.classification_id.sale_ecotax_ids + ecotax_ids = sale_ecotaxs.filtered( lambda tax: tax.company_id == self.order_id.company_id ) diff --git a/account_ecotax_sale_tax/tests/test_sale_ecotax.py b/account_ecotax_sale_tax/tests/test_sale_ecotax.py index 2998ee987..3e7ef855b 100644 --- a/account_ecotax_sale_tax/tests/test_sale_ecotax.py +++ b/account_ecotax_sale_tax/tests/test_sale_ecotax.py @@ -58,3 +58,6 @@ def test_02_classification_ecotax(self): self.assertEqual(sale_line2.subtotal_ecotax, 32) self.assertEqual(self.sale.amount_total, 1047.0) self.assertEqual(self.sale.amount_ecotax, 47.0) + + def test_03_ecotax_by_country(self): + self._test_03_ecotax_by_country() diff --git a/account_ecotax_tax/README.rst b/account_ecotax_tax/README.rst index d9ed82693..a53372536 100644 --- a/account_ecotax_tax/README.rst +++ b/account_ecotax_tax/README.rst @@ -1,7 +1,3 @@ -.. image:: https://odoo-community.org/readme-banner-image - :target: https://odoo-community.org/get-involved?utm_source=readme - :alt: Odoo Community Association - ================================= Ecotax Management (with Odoo tax) ================================= @@ -17,7 +13,7 @@ Ecotax Management (with Odoo tax) .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status :alt: Beta -.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Faccount--fiscal--rule-lightgray.png?logo=github @@ -89,6 +85,8 @@ Known issues / Roadmap Since an update in Odoo https://github.com/odoo/odoo/commit/13e9833e0bc809a26843890363586f61a37d061c the case with ecotax as tax included and another tax included does not work anymore. The ecotax tax should only be used along with price excluded tax, or be configured as price excluded itself. +There is a limitation with the country restriction on ecotax classification when using the account_ecotax_tax and account_ecotax_sale_tax modules.OIt is currently not possible to have multiple classificaiton restricted to different countries for a same product. + Bug Tracker =========== diff --git a/account_ecotax_tax/models/__init__.py b/account_ecotax_tax/models/__init__.py index 62a42c959..45704d551 100644 --- a/account_ecotax_tax/models/__init__.py +++ b/account_ecotax_tax/models/__init__.py @@ -1,3 +1,4 @@ from . import account_ecotax_classification from . import account_move_line from . import account_tax +from . import product_product diff --git a/account_ecotax_tax/models/account_move_line.py b/account_ecotax_tax/models/account_move_line.py index 0db95748b..493512c41 100644 --- a/account_ecotax_tax/models/account_move_line.py +++ b/account_ecotax_tax/models/account_move_line.py @@ -67,23 +67,40 @@ def _get_new_vals_list(self): def _compute_ecotax_line_ids(self): return super()._compute_ecotax_line_ids() + @api.depends( + "product_id", + "product_uom_id", + "move_id.partner_id", + "move_id.partner_shipping_id", + ) + def _compute_tax_ids(self): + return super()._compute_tax_ids() + def _get_computed_taxes(self): tax_ids = super()._get_computed_taxes() ecotax_ids = self.env["account.tax"] + country = ( + self.move_id.partner_shipping_id.country_id + or self.move_id.partner_id.country_id + ) if self.move_id.is_sale_document(include_receipts=True): # Out invoice. - sale_ecotaxs = self.product_id.all_ecotax_line_product_ids.mapped( - "classification_id" - ).mapped("sale_ecotax_ids") + eligible_classifications = ( + self.product_id._get_country_eligible_classification(country) + ) + sale_ecotaxs = eligible_classifications.classification_id.sale_ecotax_ids ecotax_ids = sale_ecotaxs.filtered( lambda tax: tax.company_id == self.move_id.company_id ) elif self.move_id.is_purchase_document(include_receipts=True): # In invoice. - purchase_ecotaxs = self.product_id.all_ecotax_line_product_ids.mapped( - "classification_id" - ).mapped("purchase_ecotax_ids") + eligible_classifications = ( + self.product_id._get_country_eligible_classification(country) + ) + purchase_ecotaxs = ( + eligible_classifications.classification_id.purchase_ecotax_ids + ) ecotax_ids = purchase_ecotaxs.filtered( lambda tax: tax.company_id == self.move_id.company_id ) diff --git a/account_ecotax_tax/models/account_tax.py b/account_ecotax_tax/models/account_tax.py index fd592c692..2d972254c 100644 --- a/account_ecotax_tax/models/account_tax.py +++ b/account_ecotax_tax/models/account_tax.py @@ -28,5 +28,8 @@ def onchange_is_ecotax(self): # partner: res.partner object or None # for weight based ecotax # result = quantity and product.weight_based_ecotax * quantity or 0.0 -result = quantity and product.fixed_ecotax * quantity or 0.0 +# for fix ecotax +# result = quantity and product.fixed_ecotax * quantity or 0.0 +# to manage both weight and fix with only one ecotax tax +result = quantity and product.ecotax_amount * quantity or 0.0 """ diff --git a/account_ecotax_tax/models/product_product.py b/account_ecotax_tax/models/product_product.py new file mode 100644 index 000000000..6e6934199 --- /dev/null +++ b/account_ecotax_tax/models/product_product.py @@ -0,0 +1,22 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import _, api, exceptions, models +from odoo.fields import first + + +class ProductProduct(models.Model): + _inherit = "product.product" + + @api.constrains("additional_ecotax_line_product_ids", "ecotax_line_product_ids") + def check_ecotax_line_country(self): + for product in self: + countries = first(product.all_ecotax_line_product_ids).country_ids + for ecotax_line in product.all_ecotax_line_product_ids: + if ecotax_line.country_ids != countries: + raise exceptions.UserError( + _( + "All ecotax classification for a product should have the same " + "countries allowed. This is a restriction of the " + "account_ecotax_tax module." + ) + ) diff --git a/account_ecotax_tax/readme/ROADMAP.rst b/account_ecotax_tax/readme/ROADMAP.rst index 239d19fe2..999a4af62 100644 --- a/account_ecotax_tax/readme/ROADMAP.rst +++ b/account_ecotax_tax/readme/ROADMAP.rst @@ -1,2 +1,4 @@ Since an update in Odoo https://github.com/odoo/odoo/commit/13e9833e0bc809a26843890363586f61a37d061c the case with ecotax as tax included and another tax included does not work anymore. The ecotax tax should only be used along with price excluded tax, or be configured as price excluded itself. + +There is a limitation with the country restriction on ecotax classification when using the account_ecotax_tax and account_ecotax_sale_tax modules.OIt is currently not possible to have multiple classificaiton restricted to different countries for a same product. diff --git a/account_ecotax_tax/static/description/index.html b/account_ecotax_tax/static/description/index.html index d65a4d96c..bd9e7f7f5 100644 --- a/account_ecotax_tax/static/description/index.html +++ b/account_ecotax_tax/static/description/index.html @@ -3,7 +3,7 @@ -README.rst +Ecotax Management (with Odoo tax) -
+
+

Ecotax Management (with Odoo tax)

- - -Odoo Community Association - -
-

Ecotax Management (with Odoo tax)

-

Beta License: AGPL-3 OCA/account-fiscal-rule Translate me on Weblate Try me on Runboat

+

Beta License: AGPL-3 OCA/account-fiscal-rule Translate me on Weblate Try me on Runboat

This module allows to compute the ecotax amounts from Odoo tax mechanism. The advantages compared to the base account_ecotax module is that it allows to : - Manage ecotax amount as included or excluded from the price of the product @@ -395,7 +390,7 @@

Ecotax Management (with Odoo tax)

-

Usage

+

Usage

  1. Create a tax group named “Ecotaxes”. The sequence must be lower than other tax groups. - Set the Preceding Subtotal field to “Without Ecotax”.

    @@ -441,12 +436,13 @@

    Usage

-

Known issues / Roadmap

+

Known issues / Roadmap

Since an update in Odoo https://github.com/odoo/odoo/commit/13e9833e0bc809a26843890363586f61a37d061c the case with ecotax as tax included and another tax included does not work anymore. The ecotax tax should only be used along with price excluded tax, or be configured as price excluded itself.

+

There is a limitation with the country restriction on ecotax classification when using the account_ecotax_tax and account_ecotax_sale_tax modules.OIt is currently not possible to have multiple classificaiton restricted to different countries for a same product.

-

Bug Tracker

+

Bug Tracker

Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed @@ -454,22 +450,22 @@

Bug Tracker

Do not contact contributors directly about support or help with technical issues.

-
diff --git a/account_ecotax_tax/tests/test_ecotax.py b/account_ecotax_tax/tests/test_ecotax.py index 84df19023..1b9f533e5 100644 --- a/account_ecotax_tax/tests/test_ecotax.py +++ b/account_ecotax_tax/tests/test_ecotax.py @@ -24,7 +24,7 @@ def setUpClass(cls): "company_id": cls.env.user.company_id.id, } ) - cls.invoice_fixed_ecotax = cls.env["account.tax"].create( + cls.invoice_ecotax = cls.env["account.tax"].create( { "name": "Fixed Ecotax", "type_tax_use": "sale", @@ -35,60 +35,7 @@ def setUpClass(cls): "sequence": 0, "is_ecotax": True, "python_compute": "result = (quantity and" - " product.fixed_ecotax * quantity or 0.0)", - "tax_exigibility": "on_invoice", - "invoice_repartition_line_ids": [ - ( - 0, - 0, - { - "factor_percent": 100, - "repartition_type": "base", - }, - ), - ( - 0, - 0, - { - "factor_percent": 100, - "repartition_type": "tax", - "account_id": cls.invoice_ecotax_account.id, - }, - ), - ], - "refund_repartition_line_ids": [ - ( - 0, - 0, - { - "factor_percent": 100, - "repartition_type": "base", - }, - ), - ( - 0, - 0, - { - "factor_percent": 100, - "repartition_type": "tax", - "account_id": cls.invoice_ecotax_account.id, - }, - ), - ], - } - ) - cls.invoice_weight_based_ecotax = cls.env["account.tax"].create( - { - "name": "Weight Based Ecotax", - "type_tax_use": "sale", - "company_id": cls.env.user.company_id.id, - "amount_type": "code", - "include_base_amount": True, - "price_include": False, - "sequence": 0, - "is_ecotax": True, - "python_compute": "result = (quantity and" - " product.weight_based_ecotax * quantity or 0.0)", + " product.ecotax_amount * quantity or 0.0)", "tax_exigibility": "on_invoice", "invoice_repartition_line_ids": [ ( @@ -132,9 +79,9 @@ def setUpClass(cls): ) # ECOTAXES # 1- Fixed ecotax - cls.ecotax_fixed.sale_ecotax_ids = cls.invoice_fixed_ecotax + cls.ecotax_fixed.sale_ecotax_ids = cls.invoice_ecotax # 2- Weight-based ecotax - cls.ecotax_weight.sale_ecotax_ids = cls.invoice_weight_based_ecotax + cls.ecotax_weight.sale_ecotax_ids = cls.invoice_ecotax class TestInvoiceEcotaxTax(TestInvoiceEcotaxTaxComon): @@ -327,3 +274,6 @@ def test_05_product_variants(self): variant_2.all_ecotax_line_product_ids, variant_2.product_tmpl_id.ecotax_line_product_ids, ) + + def test_06_ecotax_by_country(self): + self._test_06_ecotax_by_country()