diff --git a/l10n_fr_ecotaxe_product/README.rst b/l10n_fr_ecotaxe_product/README.rst new file mode 100644 index 000000000..29a089b27 --- /dev/null +++ b/l10n_fr_ecotaxe_product/README.rst @@ -0,0 +1,80 @@ +========================================== +France Custom Ecotaxe - Manage on Products +========================================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/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%2Fl10n--france-lightgray.png?logo=github + :target: https://github.com/OCA/l10n-france/tree/15.0/l10n_fr_ecotaxe_product + :alt: OCA/l10n-france +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/l10n-france-15-0/l10n-france-15-0-l10n_fr_ecotaxe_product + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/121/15.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module extends "France Custom Ecotaxe" by allowing ecotaxes to be managed +on products instead of templates. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +Usage is similar to module "France Custom Ecotaxe", with the exception that +ecotaxes are managed on products instead of templates. + +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 smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Camptocamp + +Contributors +~~~~~~~~~~~~ + +* Silvio Gregorini + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/l10n-france `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/l10n_fr_ecotaxe_product/__init__.py b/l10n_fr_ecotaxe_product/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/l10n_fr_ecotaxe_product/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/l10n_fr_ecotaxe_product/__manifest__.py b/l10n_fr_ecotaxe_product/__manifest__.py new file mode 100644 index 000000000..23909324c --- /dev/null +++ b/l10n_fr_ecotaxe_product/__manifest__.py @@ -0,0 +1,15 @@ +# Copyright 2021 Camptocamp +# @author Silvio Gregorini +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +{ + "name": "France Custom Ecotaxe - Manage on Products", + "summary": "Move Ecotax management from templates to products", + "version": "16.0.1.0.0", + "author": "Camptocamp, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/l10n-france", + "category": "Localization/Account Taxes", + "license": "AGPL-3", + "depends": ["l10n_fr_ecotaxe"], + "installable": True, +} diff --git a/l10n_fr_ecotaxe_product/i18n/l10n_fr_ecotaxe_product.pot b/l10n_fr_ecotaxe_product/i18n/l10n_fr_ecotaxe_product.pot new file mode 100644 index 000000000..a6c52d7ee --- /dev/null +++ b/l10n_fr_ecotaxe_product/i18n/l10n_fr_ecotaxe_product.pot @@ -0,0 +1,46 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * l10n_fr_ecotaxe_product +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: l10n_fr_ecotaxe_product +#: model:ir.model.fields,field_description:l10n_fr_ecotaxe_product.field_product_product__ecotaxe_amount +msgid "Ecotaxe Amount" +msgstr "" + +#. module: l10n_fr_ecotaxe_product +#: model:ir.model.fields,help:l10n_fr_ecotaxe_product.field_product_product__ecotaxe_amount +msgid "Ecotaxe Amount computed form Classification" +msgstr "" + +#. module: l10n_fr_ecotaxe_product +#: model:ir.model.fields,field_description:l10n_fr_ecotaxe_product.field_product_product__ecotaxe_classification_id +msgid "Ecotaxe Classification" +msgstr "" + +#. module: l10n_fr_ecotaxe_product +#: model:ir.model.fields,field_description:l10n_fr_ecotaxe_product.field_product_product__manual_fixed_ecotaxe +msgid "Manual Fixed Ecotaxe" +msgstr "" + +#. module: l10n_fr_ecotaxe_product +#: model:ir.model.fields,help:l10n_fr_ecotaxe_product.field_product_product__manual_fixed_ecotaxe +msgid "" +"Manual Fixed ecotaxe.\n" +"Allow to subtite default Ecotaxe Classification\n" +msgstr "" + +#. module: l10n_fr_ecotaxe_product +#: model:ir.model,name:l10n_fr_ecotaxe_product.model_product_product +msgid "Product" +msgstr "" diff --git a/l10n_fr_ecotaxe_product/models/__init__.py b/l10n_fr_ecotaxe_product/models/__init__.py new file mode 100644 index 000000000..021a0c85b --- /dev/null +++ b/l10n_fr_ecotaxe_product/models/__init__.py @@ -0,0 +1,5 @@ +# Copyright 2021 Camptocamp +# @author Silvio Gregorini +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import product_product diff --git a/l10n_fr_ecotaxe_product/models/product_product.py b/l10n_fr_ecotaxe_product/models/product_product.py new file mode 100644 index 000000000..e84c22009 --- /dev/null +++ b/l10n_fr_ecotaxe_product/models/product_product.py @@ -0,0 +1,69 @@ +# Copyright 2021 Camptocamp +# @author Silvio Gregorini +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import api, fields, models + + +class ProductProduct(models.Model): + _inherit = "product.product" + + ecotaxe_classification_id = fields.Many2one( + "account.ecotaxe.classification", + string="Ecotaxe Classification", + ) + ecotaxe_amount = fields.Float( + compute="_compute_ecotaxe", + help="Ecotaxe Amount computed form Classification", + store=True, + ) + manual_fixed_ecotaxe = fields.Float( + help="Manual Fixed ecotaxe.\n" + "Allow to subtite default Ecotaxe Classification\n" + ) + + @api.model_create_multi + def create(self, vals_list): + prods = super().create(vals_list) + prods._post_create_set_ecotax_classification() + return prods + + def _post_create_set_ecotax_classification(self): + for prod in self: + tmpl = prod.product_tmpl_id + # Case 1: + # * newly created product has an ecotax classification + # * the template has no classification + # * the product is the template's only variant + # => copy ecotax from product to template + if ( + prod.ecotaxe_classification_id + and not tmpl.ecotaxe_classification_id + and tmpl.product_variant_ids == prod + ): + tmpl.ecotaxe_classification_id = prod.ecotaxe_classification_id + # Case 2: + # * newly created product has no ecotax classification + # * the template has ecotax classification + # => copy ecotax from template to product + elif not prod.ecotaxe_classification_id and tmpl.ecotaxe_classification_id: + prod.ecotaxe_classification_id = tmpl.ecotaxe_classification_id + + @api.depends( + "ecotaxe_classification_id", + "ecotaxe_classification_id.ecotaxe_type", + "ecotaxe_classification_id.ecotaxe_coef", + "manual_fixed_ecotaxe", + "weight", + "product_tmpl_id.ecotaxe_amount", + ) + def _compute_ecotaxe(self): + for prod in self: + ecotax_cls = prod.ecotaxe_classification_id + if ecotax_cls.ecotaxe_type == "weight_based": + amt = ecotax_cls.ecotaxe_coef * (prod.weight or 0.0) + elif prod.manual_fixed_ecotaxe: + amt = prod.manual_fixed_ecotaxe + else: + amt = ecotax_cls.default_fixed_ecotaxe + prod.ecotaxe_amount = amt diff --git a/l10n_fr_ecotaxe_product/readme/CONTRIBUTORS.rst b/l10n_fr_ecotaxe_product/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..285df6e88 --- /dev/null +++ b/l10n_fr_ecotaxe_product/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Silvio Gregorini diff --git a/l10n_fr_ecotaxe_product/readme/DESCRIPTION.rst b/l10n_fr_ecotaxe_product/readme/DESCRIPTION.rst new file mode 100644 index 000000000..deae8f4f2 --- /dev/null +++ b/l10n_fr_ecotaxe_product/readme/DESCRIPTION.rst @@ -0,0 +1,2 @@ +This module extends "France Custom Ecotaxe" by allowing ecotaxes to be managed +on products instead of templates. diff --git a/l10n_fr_ecotaxe_product/readme/USAGE.rst b/l10n_fr_ecotaxe_product/readme/USAGE.rst new file mode 100644 index 000000000..d4389ee97 --- /dev/null +++ b/l10n_fr_ecotaxe_product/readme/USAGE.rst @@ -0,0 +1,2 @@ +Usage is similar to module "France Custom Ecotaxe", with the exception that +ecotaxes are managed on products instead of templates. diff --git a/l10n_fr_ecotaxe_product/static/description/icon.png b/l10n_fr_ecotaxe_product/static/description/icon.png new file mode 100644 index 000000000..ee185a69f Binary files /dev/null and b/l10n_fr_ecotaxe_product/static/description/icon.png differ diff --git a/l10n_fr_ecotaxe_product/static/description/index.html b/l10n_fr_ecotaxe_product/static/description/index.html new file mode 100644 index 000000000..fb446e800 --- /dev/null +++ b/l10n_fr_ecotaxe_product/static/description/index.html @@ -0,0 +1,426 @@ + + + + + + +France Custom Ecotaxe - Manage on Products + + + +
+

France Custom Ecotaxe - Manage on Products

+ + +

Beta License: AGPL-3 OCA/l10n-france Translate me on Weblate Try me on Runbot

+

This module extends “France Custom Ecotaxe” by allowing ecotaxes to be managed +on products instead of templates.

+

Table of contents

+ +
+

Usage

+

Usage is similar to module “France Custom Ecotaxe”, with the exception that +ecotaxes are managed on products instead of templates.

+
+
+

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 smashing it by providing a detailed and welcomed +feedback.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • Camptocamp
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/l10n-france project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/l10n_fr_ecotaxe_product/tests/__init__.py b/l10n_fr_ecotaxe_product/tests/__init__.py new file mode 100644 index 000000000..88ecf9277 --- /dev/null +++ b/l10n_fr_ecotaxe_product/tests/__init__.py @@ -0,0 +1,6 @@ +# Copyright 2021 Camptocamp +# @author Silvio Gregorini +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import test_invoice_ecotaxe +from . import test_product_ecotaxe diff --git a/l10n_fr_ecotaxe_product/tests/common.py b/l10n_fr_ecotaxe_product/tests/common.py new file mode 100644 index 000000000..c3ffceac6 --- /dev/null +++ b/l10n_fr_ecotaxe_product/tests/common.py @@ -0,0 +1,40 @@ +# Copyright 2021 Camptocamp +# @author Silvio Gregorini +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + + +def get_ecotax_cls(env, ecotaxe_type, search_domain=None, create_vals=None): + ecotax_cls = _search_ecotax_cls(env, ecotaxe_type, search_domain) + if not ecotax_cls: + ecotax_cls = _create_ecotax_cls(env, ecotaxe_type, create_vals) + return ecotax_cls + + +def _search_ecotax_cls(env, ecotaxe_type, search_domain=None): + return env["account.ecotaxe.classification"].search( + [("ecotaxe_type", "=", ecotaxe_type)] + list(search_domain or []) + ) + + +def _create_ecotax_cls(env, ecotaxe_type, create_vals=None): + assert ecotaxe_type in ("fixed", "weight_based") + if ecotaxe_type == "fixed": + vals = { + "name": "Fixed Ecotax", + "ecotaxe_type": "fixed", + "default_fixed_ecotaxe": 5.0, + "ecotaxe_product_status": "M", + "ecotaxe_supplier_status": "FAB", + } + else: + vals = { + "name": "Weight Based Ecotax", + "ecotaxe_type": "weight_based", + "ecotaxe_coef": 0.04, + "ecotaxe_product_status": "P", + "ecotaxe_supplier_status": "FAB", + } + create_vals = dict(create_vals or {}) + create_vals.pop("ecotaxe_type", None) + vals.update(create_vals) + return env["account.ecotaxe.classification"].create(vals) diff --git a/l10n_fr_ecotaxe_product/tests/test_invoice_ecotaxe.py b/l10n_fr_ecotaxe_product/tests/test_invoice_ecotaxe.py new file mode 100644 index 000000000..36a9ef321 --- /dev/null +++ b/l10n_fr_ecotaxe_product/tests/test_invoice_ecotaxe.py @@ -0,0 +1,301 @@ +# Copyright 2021 Camptocamp +# @author Silvio Gregorini +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from random import choice + +from odoo.tests.common import Form, tagged + +from odoo.addons.account.tests.common import AccountTestInvoicingCommon + +from .common import get_ecotax_cls + + +# Using TestProductCommon to access `init_invoice` already defined in there +@tagged("post_install", "-at_install") +class TestProductEcotaxe(AccountTestInvoicingCommon): + @classmethod + def setUpClass(cls, chart_template_ref=None): + super().setUpClass(chart_template_ref) + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + + # ECOTAXES + # 1- Fixed ecotax + cls.ecotax_fixed = get_ecotax_cls( + cls.env, + "fixed", + search_domain=[("company_id", "=", cls.env.user.company_id.id)], + ) + # 2- Fixed ecotax + cls.ecotax_weight = get_ecotax_cls( + cls.env, + "weight_based", + search_domain=[("company_id", "=", cls.env.user.company_id.id)], + ) + + # ACCOUNTING STUFF + # 1- Tax account + cls.invoice_tax_account = cls.env["account.account"].create( + { + "code": "invoice_tax_account", + "name": "Invoice Tax Account", + "user_type_id": cls.env.ref("account.data_account_type_revenue").id, + "company_id": cls.env.user.company_id.id, + } + ) + # 2- Invoice tax with included price to avoid unwanted amounts in tests + cls.invoice_tax = cls.env["account.tax"].create( + { + "name": "Tax 10%", + "price_include": True, + "type_tax_use": "sale", + "company_id": cls.env.user.company_id.id, + "amount": 10, + "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_tax_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_tax_account.id, + }, + ), + ], + } + ) + + # MISC + # 1- Invoice partner + cls.invoice_partner = cls.env["res.partner"].create({"name": "Test"}) + + @classmethod + def _create_invoice(cls, products): + """Creates a new customer invoice with given products and returns it""" + return cls.init_invoice( + "out_invoice", + partner=cls.invoice_partner, + products=products, + company=cls.env.user.company_id, + taxes=cls.invoice_tax, + ) + + @classmethod + def _create_product(cls, ecotax_classification): + """Creates a product template with given ecotax classification + + Returns the newly created template variant + """ + tmpl = cls.env["product.template"].create( + { + "name": " - ".join(["Product", ecotax_classification.name]), + "ecotaxe_classification_id": ecotax_classification.id, + # For the sake of simplicity, every product will have a price + # and weight of 100 + "list_price": 100.00, + "weight": 100.00, + } + ) + return tmpl.product_variant_ids[0] + + @staticmethod + def _set_invoice_lines_random_quantities(invoice) -> list: + """For each invoice line, sets a random qty between 1 and 10 + + Returns the list of new quantities as a list + """ + new_qtys = [] + random_qtys = tuple(range(1, 11)) + with Form(invoice) as invoice_form: + for index in range(len(invoice.invoice_line_ids)): + with invoice_form.invoice_line_ids.edit(index) as line_form: + new_qty = choice(random_qtys) + line_form.quantity = new_qty + new_qtys.insert(index, new_qty) + line_form.save() + invoice_form.save() + return new_qtys + + def _run_checks(self, inv, inv_expected_amounts, inv_lines_expected_amounts): + self.assertEqual(inv.amount_ecotaxe, inv_expected_amounts["amount_ecotaxe"]) + self.assertEqual(inv.amount_total, inv_expected_amounts["amount_total"]) + self.assertEqual(len(inv.invoice_line_ids), len(inv_lines_expected_amounts)) + for inv_line, inv_line_expected_amounts in zip( + inv.invoice_line_ids, inv_lines_expected_amounts + ): + self.assertEqual( + inv_line.unit_ecotaxe_amount, + inv_line_expected_amounts["unit_ecotaxe_amount"], + ) + self.assertEqual( + inv_line.subtotal_ecotaxe, inv_line_expected_amounts["subtotal_ecotaxe"] + ) + + def test_01_default_fixed_ecotax(self): + """Test default fixed ecotax + + Ecotax classification data for this test: + - fixed type + - default amount: 5.0 + Product data for this test: + - list price: 100 + - fixed ecotax + - no manual amount + + Expected results (with 1 line and qty = 1): + - invoice ecotax amount: 5.0 + - invoice total amount: 100.0 + - line ecotax unit amount: 5.0 + - line ecotax total amount: 5.0 + """ + invoice = self._create_invoice(products=self._create_product(self.ecotax_fixed)) + self._run_checks( + invoice, + {"amount_ecotaxe": 5.0, "amount_total": 100.0}, + [{"unit_ecotaxe_amount": 5.0, "subtotal_ecotaxe": 5.0}], + ) + new_qty = self._set_invoice_lines_random_quantities(invoice)[0] + self._run_checks( + invoice, + {"amount_ecotaxe": 5.0 * new_qty, "amount_total": 100.0 * new_qty}, + [{"unit_ecotaxe_amount": 5.0, "subtotal_ecotaxe": 5.0 * new_qty}], + ) + + def test_02_manual_fixed_ecotax(self): + """Test manual fixed ecotax + + Ecotax classification data for this test: + - fixed type + - default amount: 5.0 + Product data for this test: + - list price: 100 + - fixed ecotax + - manual amount: 10 + + Expected results (with 1 line and qty = 1): + - invoice ecotax amount: 10.0 + - invoice total amount: 100.0 + - line ecotax unit amount: 10.0 + - line ecotax total amount: 10.0 + """ + product = self._create_product(self.ecotax_fixed) + product.manual_fixed_ecotaxe = 10 + invoice = self._create_invoice(products=product) + self._run_checks( + invoice, + {"amount_ecotaxe": 10.0, "amount_total": 100.0}, + [{"unit_ecotaxe_amount": 10.0, "subtotal_ecotaxe": 10.0}], + ) + new_qty = self._set_invoice_lines_random_quantities(invoice)[0] + self._run_checks( + invoice, + {"amount_ecotaxe": 10.0 * new_qty, "amount_total": 100.0 * new_qty}, + [{"unit_ecotaxe_amount": 10.0, "subtotal_ecotaxe": 10.0 * new_qty}], + ) + + def test_03_weight_based_ecotax(self): + """Test weight based ecotax + + Ecotax classification data for this test: + - weight based type + - coefficient: 0.04 + Product data for this test: + - list price: 100 + - weight based ecotax + - weight: 100 + + Expected results (with 1 line and qty = 1): + - invoice ecotax amount: 4.0 + - invoice total amount: 100.0 + - line ecotax unit amount: 4.0 + - line ecotax total amount: 4.0 + """ + invoice = self._create_invoice( + products=self._create_product(self.ecotax_weight) + ) + self._run_checks( + invoice, + {"amount_ecotaxe": 4.0, "amount_total": 100.0}, + [{"unit_ecotaxe_amount": 4.0, "subtotal_ecotaxe": 4.0}], + ) + new_qty = self._set_invoice_lines_random_quantities(invoice)[0] + self._run_checks( + invoice, + {"amount_ecotaxe": 4.0 * new_qty, "amount_total": 100.0 * new_qty}, + [{"unit_ecotaxe_amount": 4.0, "subtotal_ecotaxe": 4.0 * new_qty}], + ) + + def test_04_mixed_ecotax(self): + """Test mixed ecotax within the same invoice + + Creating an invoice with 3 lines (one per type with types tested above) + + Expected results (with 3 lines and qty = 1): + - invoice ecotax amount: 19.0 + - invoice total amount: 300.0 + - line ecotax unit amount (fixed ecotax): 5.0 + - line ecotax total amount (fixed ecotax): 5.0 + - line ecotax unit amount (manual ecotax): 10.0 + - line ecotax total amount (manual ecotax): 10.0 + - line ecotax unit amount (weight based ecotax): 4.0 + - line ecotax total amount (weight based ecotax): 4.0 + """ + default_fixed_product = self._create_product(self.ecotax_fixed) + manual_fixed_product = self._create_product(self.ecotax_fixed) + manual_fixed_product.manual_fixed_ecotaxe = 10 + weight_based_product = self._create_product(self.ecotax_weight) + invoice = self._create_invoice( + products=default_fixed_product | manual_fixed_product | weight_based_product + ) + self._run_checks( + invoice, + {"amount_ecotaxe": 19.0, "amount_total": 300.0}, + [ + {"unit_ecotaxe_amount": 5.0, "subtotal_ecotaxe": 5.0}, + {"unit_ecotaxe_amount": 10.0, "subtotal_ecotaxe": 10.0}, + {"unit_ecotaxe_amount": 4.0, "subtotal_ecotaxe": 4.0}, + ], + ) + new_qtys = self._set_invoice_lines_random_quantities(invoice) + self._run_checks( + invoice, + { + "amount_ecotaxe": 5.0 * new_qtys[0] + + 10.0 * new_qtys[1] + + 4.0 * new_qtys[2], + "amount_total": 100.0 * sum(new_qtys), + }, + [ + {"unit_ecotaxe_amount": 5.0, "subtotal_ecotaxe": 5.0 * new_qtys[0]}, + {"unit_ecotaxe_amount": 10.0, "subtotal_ecotaxe": 10.0 * new_qtys[1]}, + {"unit_ecotaxe_amount": 4.0, "subtotal_ecotaxe": 4.0 * new_qtys[2]}, + ], + ) diff --git a/l10n_fr_ecotaxe_product/tests/test_product_ecotaxe.py b/l10n_fr_ecotaxe_product/tests/test_product_ecotaxe.py new file mode 100644 index 000000000..735eb9ab9 --- /dev/null +++ b/l10n_fr_ecotaxe_product/tests/test_product_ecotaxe.py @@ -0,0 +1,181 @@ +# Copyright 2021 Camptocamp +# @author Silvio Gregorini +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from random import choice + +from odoo.tests.common import Form, tagged + +from odoo.addons.product.tests.common import TestProductCommon + +from .common import get_ecotax_cls + + +# Using TestProductCommon to access product.attribute already defined in there +@tagged("post_install", "-at_install") +class TestProductEcotaxe(TestProductCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + + # ECOTAXES + dom = [("company_id", "=", cls.env.user.company_id.id)] + vals = {"company_id": cls.env.user.company_id.id} + # 1- Fixed ecotax + cls.ecotax_fixed = get_ecotax_cls(cls.env, "fixed", dom, vals) + # 2- Fixed ecotax + cls.ecotax_weight = get_ecotax_cls(cls.env, "weight_based", dom, vals) + + @classmethod + def _create_product(cls, ecotax_cls): + with Form( + cls.env["product.product"], "product.product_normal_form_view" + ) as prod_form: + prod_form.name = "Product" + prod_form.ecotaxe_classification_id = ecotax_cls + return prod_form.save() + + @classmethod + def _create_single_variant_template(cls, ecotax_cls): + with Form( + cls.env["product.template"], "product.product_template_only_form_view" + ) as tmpl_form: + tmpl_form.name = "Single-variant Template" + tmpl_form.ecotaxe_classification_id = ecotax_cls + return tmpl_form.save() + + @classmethod + def _create_multi_variant_template(cls, ecotax_cls, attributes): + with Form( + cls.env["product.template"], "product.product_template_only_form_view" + ) as tmpl_form: + tmpl_form.name = "Multi-variant Template" + tmpl_form.ecotaxe_classification_id = ecotax_cls + for attrib, values in attributes: + with tmpl_form.attribute_line_ids.new() as attrib_line: + attrib_line.attribute_id = attrib + for val in values: + attrib_line.value_ids.add(val) + return tmpl_form.save() + + def test_01_templates_and_products_creation(self): + cls = choice([self.ecotax_fixed, self.ecotax_weight]) + + # Create a template with 1 variant, check that its product has the + # correct ecotax class + tmpl_1_variant = self._create_single_variant_template(cls) + self.assertTrue( + tmpl_1_variant.product_variant_ids.ecotaxe_classification_id + == tmpl_1_variant.product_variant_ids.ecotaxe_classification_id + == cls + ) + + # Create a template with 3 variants, check that its products have all + # the correct ecotax class + tmpl_3_variant = self._create_multi_variant_template( + cls, + [ + ( + self.prod_att_1, + self.prod_attr1_v1 | self.prod_attr1_v2 | self.prod_attr1_v3, + ) + ], + ) + self.assertTrue( + tmpl_3_variant.product_variant_ids.ecotaxe_classification_id + == tmpl_3_variant.ecotaxe_classification_id + == cls + ) + + # Create a product, check that the template that has been implicitly + # created has the same ecotax + prod = self._create_product(cls) + self.assertTrue( + prod.ecotaxe_classification_id + == prod.product_tmpl_id.ecotaxe_classification_id + == cls + ) + + def test_02_single_variant_template(self): + cls1 = choice([self.ecotax_fixed, self.ecotax_weight]) + cls2 = (self.ecotax_fixed | self.ecotax_weight) - cls1 + + # Create a template with 1 variant and ecotax class ``cls1`` + tmpl = self._create_single_variant_template(cls1) + prod = tmpl.product_variant_ids + + # Change product's ecotax classification to ``cls2`` + with Form(prod, "product.product_normal_form_view") as prod_form: + prod_form.ecotaxe_classification_id = cls2 + prod = prod_form.save() + + # Check they have different ecotax classifications: template should + # still have the old classification + self.assertEqual(tmpl.ecotaxe_classification_id, cls1) + self.assertEqual(prod.ecotaxe_classification_id, cls2) + + def test_03_multiple_variants_template(self): + cls1 = choice([self.ecotax_fixed, self.ecotax_weight]) + cls2 = (self.ecotax_fixed | self.ecotax_weight) - cls1 + + # Create a template with 3 variants + tmpl = self._create_multi_variant_template( + cls1, + [ + ( + self.prod_att_1, + self.prod_attr1_v1 | self.prod_attr1_v2 | self.prod_attr1_v3, + ) + ], + ) + prods = tmpl.product_variant_ids + + # Change a random product's ecotax classification + with Form( + choice(prods), "product.product_normal_form_view" + ) as random_prod_form: + random_prod_form.ecotaxe_classification_id = cls2 + random_prod = random_prod_form.save() + other_prods = prods - random_prod + + # Check they have different ecotax classification: template and 2 + # products should still have the old classification, while + # 3rd product's ecotax class must've changed + self.assertTrue( + other_prods.ecotaxe_classification_id + == tmpl.ecotaxe_classification_id + == cls1 + ) + self.assertEqual(random_prod.ecotaxe_classification_id, cls2) + + def test_04_prod_ecotaxe_amount(self): + # Not using random classes here because we need to test amounts, so we + # need predictable classification behaviours instead of randomness + fixed_cls, weight_based_cls = self.ecotax_fixed, self.ecotax_weight + + # Create a template with 1 variant + tmpl = self._create_single_variant_template(fixed_cls) + prod = tmpl.product_variant_ids + + # Change product's ecotax to "weight-based", set its weight to 100 + weight_based_cls.ecotaxe_coef = 0.2 + prod.weight = 100 + prod.ecotaxe_classification_id = weight_based_cls + + # Change template manual value to 10 + tmpl.manual_fixed_ecotaxe = 10 + + # Check ecotax on both + self.assertEqual(prod.ecotaxe_amount, 20) + self.assertEqual(tmpl.ecotaxe_amount, 10) + + # Revert back product's classification to "fixed", but set both + # classification default amount and product manual amount to 0 + prod.manual_fixed_ecotaxe = 0 + fixed_cls.default_fixed_ecotaxe = 0 + prod.ecotaxe_classification_id = fixed_cls + + # Check that product and template have different ecotax amounts + self.assertEqual(prod.ecotaxe_amount, 0) + self.assertEqual(tmpl.ecotaxe_amount, 10) diff --git a/setup/l10n_fr_ecotaxe_product/odoo/addons/l10n_fr_ecotaxe_product b/setup/l10n_fr_ecotaxe_product/odoo/addons/l10n_fr_ecotaxe_product new file mode 120000 index 000000000..3e14917fb --- /dev/null +++ b/setup/l10n_fr_ecotaxe_product/odoo/addons/l10n_fr_ecotaxe_product @@ -0,0 +1 @@ +../../../../l10n_fr_ecotaxe_product \ No newline at end of file diff --git a/setup/l10n_fr_ecotaxe_product/setup.py b/setup/l10n_fr_ecotaxe_product/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/l10n_fr_ecotaxe_product/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 000000000..0e4e24b27 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1 @@ +odoo-addon-l10n_fr_ecotaxe @git+https://github.com/OCA/l10n-france.git@refs/pull/448/head#subdirectory=setup/l10n_fr_ecotaxe