diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3bf18e843..8bafed382 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,8 +36,10 @@ jobs: matrix: include: - container: ghcr.io/oca/oca-ci/py3.10-odoo16.0:latest + exclude: "account_product_fiscal_classification" name: test with Odoo - container: ghcr.io/oca/oca-ci/py3.10-ocb16.0:latest + exclude: "account_product_fiscal_classification" name: test with OCB makepot: "true" services: diff --git a/account_ecotax/README.rst b/account_ecotax/README.rst new file mode 100644 index 000000000..9a2d3ad83 --- /dev/null +++ b/account_ecotax/README.rst @@ -0,0 +1,142 @@ +================= +Ecotax Management +================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:23479e79cea1c7653013329021c55bf27b1d6fa0d64734b13f53ca3209feaffa + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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%2Faccount--fiscal--rule-lightgray.png?logo=github + :target: https://github.com/OCA/account-fiscal-rule/tree/16.0/account_ecotax + :alt: OCA/account-fiscal-rule +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/account-fiscal-rule-16-0/account-fiscal-rule-16-0-account_ecotax + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/account-fiscal-rule&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module applies to companies based in France mainland. It doesn't apply to +companies based in the DOM-TOMs (Guadeloupe, Martinique, Guyane, Réunion, +Mayotte). + +It add Ecotaxe amount on invoice line. +furthermore, a total ecotaxe are added at the footer of each document. + +To make easy ecotaxe management and to factor the data, ecotaxe are set on products via ECOTAXE classifications. +ECOTAXE classification can either a fixed or weight based ecotaxe. + +A product can have one or serveral ecotaxe classifications. For exemple wooden window blinds equipped with electric motor can +have ecotaxe for wood and ecotaxe for electric motor. + +This module has some limits : +- The ecotax amount is always included in the price of the product. +- The ecotax amount is not isolated in an specific accounting account but is included in the product income account. + +If one of these limits is an issue, you could install the submodule account_ecotax_tax. +This second module lets you manage the ecotax as a tax, so you can configure if you want it to be included or excluded of product price and also configuring an accounting account to isolate it. +The main consequence of this approach is that the ecotax won't be considered in the turnover, since it is considered as a tax. + +This module version add the possibility to manage several ecotaxe classification by product. +A migration script is necessary to update from previous versions. + +There is the main change to manage in migration script: + +renamed field +model old field new field +account.move.line unit_ecotaxe_amount ecotaxe_amount_unit +product.template manual_fixed_ecotaxe force_ecotaxe_amount + +changed fields +model old field new field +product.template ecotaxe_classification_id ecotaxe_classification_ids + +added fields +model new field +account.move.line ecotaxe_line_ids +product.template ecotaxe_line_product_ids + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +1. Add an ecotax classification via the menu **Accounting > Configuration > Taxes > Ecotax Classification**. + + - The ecotax classification can be either a fixed ecotax or a weight-based ecotax. + - Ecotax classification information can be used for legal declarations. + - For the fixed ecotax, the ecotax amount is used as a default value, which can be overridden on the product. + - For the weight-based ecotax, define one ecotax by a coefficient applied to the weight (depending on the product's materials). + +2. Assign one or more ecotax classifications to a product. + + - The ecotax amount can also be manually overridden on the product. + +3. Create an invoice with this product + +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 +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Akretion + +Contributors +~~~~~~~~~~~~ + +* Mourad EL HADJ MIMOUNE +* Florian da Costa + +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. + +.. |maintainer-mourad-ehm| image:: https://github.com/mourad-ehm.png?size=40px + :target: https://github.com/mourad-ehm + :alt: mourad-ehm +.. |maintainer-florian-dacosta| image:: https://github.com/florian-dacosta.png?size=40px + :target: https://github.com/florian-dacosta + :alt: florian-dacosta + +Current `maintainers `__: + +|maintainer-mourad-ehm| |maintainer-florian-dacosta| + +This module is part of the `OCA/account-fiscal-rule `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/account_ecotax/__init__.py b/account_ecotax/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/account_ecotax/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/account_ecotax/__manifest__.py b/account_ecotax/__manifest__.py new file mode 100644 index 000000000..fba33c82e --- /dev/null +++ b/account_ecotax/__manifest__.py @@ -0,0 +1,30 @@ +# © 2014-2023 Akretion (http://www.akretion.com) +# @author Mourad EL HADJ MIMOUNE +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +{ + "name": "Ecotax Management", + "summary": "Ecotax Management: in French context is a 'cost' " + "added to the sale price of electrical or electronic appliances or furnishing items", + "version": "16.0.1.0.0", + "author": "Akretion, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/account-fiscal-rule", + "category": "Localization/Account Taxes", + "license": "AGPL-3", + "maintainers": ["mourad-ehm", "florian-dacosta"], + "depends": [ + "account", + ], + "data": [ + "data/decimal_precision.xml", + "security/ir_rule.xml", + "security/ir.model.access.csv", + "views/account_ecotax_category_view.xml", + "views/ecotax_sector_view.xml", + "views/ecotax_collector_view.xml", + "views/account_ecotax_classification_view.xml", + "views/account_move_view.xml", + "views/product_template_view.xml", + "views/product_view.xml", + ], + "installable": True, +} diff --git a/account_ecotax/data/decimal_precision.xml b/account_ecotax/data/decimal_precision.xml new file mode 100644 index 000000000..332c515a2 --- /dev/null +++ b/account_ecotax/data/decimal_precision.xml @@ -0,0 +1,7 @@ + + + + Ecotax + 4 + + diff --git a/account_ecotax/models/__init__.py b/account_ecotax/models/__init__.py new file mode 100644 index 000000000..5656b8f57 --- /dev/null +++ b/account_ecotax/models/__init__.py @@ -0,0 +1,12 @@ +from . import account_ecotax_category +from . import account_ecotax_classification +from . import account_move +from . import account_move_line +from . import ecotax_line_product +from . import product_template +from . import ecotax_line_mixin +from . import account_move_line_ecotax +from . import product_product +from . import ecotax_sector +from . import ecotax_collector +from . import account_tax diff --git a/account_ecotax/models/account_ecotax_category.py b/account_ecotax/models/account_ecotax_category.py new file mode 100644 index 000000000..70827350d --- /dev/null +++ b/account_ecotax/models/account_ecotax_category.py @@ -0,0 +1,15 @@ +# Copyright 2021 Camptocamp +# @author Silvio Gregorini +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class AccountEcotaxCategory(models.Model): + _name = "account.ecotax.category" + _description = "Account Ecotax Category" + + name = fields.Char(required=True) + code = fields.Char(required=True) + description = fields.Char() + active = fields.Boolean(default=True) diff --git a/account_ecotax/models/account_ecotax_classification.py b/account_ecotax/models/account_ecotax_classification.py new file mode 100644 index 000000000..5e75b7b73 --- /dev/null +++ b/account_ecotax/models/account_ecotax_classification.py @@ -0,0 +1,92 @@ +# © 2014-2023 Akretion (http://www.akretion.com) +# @author Mourad EL HADJ MIMOUNE +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import api, fields, models + + +class AccountEcotaxClassification(models.Model): + _name = "account.ecotax.classification" + _description = "Account Ecotax Classification" + + name = fields.Char(required=True) + code = fields.Char() + ecotax_type = fields.Selection( + [("fixed", "Fixed"), ("weight_based", "Weight based")], + required=True, + help="If ecotax is weight based," + "the ecotax coef must take into account\n" + "the weight unit of measure (kg by default)", + ) + ecotax_coef = fields.Float( + digits="Ecotax", compute="_compute_ecotax_vals", readonly=False, store=True + ) + default_fixed_ecotax = fields.Float( + digits="Ecotax", + help="Default fixed ecotax amount.", + compute="_compute_ecotax_vals", + readonly=False, + store=True, + ) + categ_id = fields.Many2one( + comodel_name="account.ecotax.category", + string="Category", + ) + sector_id = fields.Many2one( + comodel_name="ecotax.sector", + string="Ecotax sector", + ) + collector_id = fields.Many2one( + comodel_name="ecotax.collector", + string="Ecotax collector", + ) + active = fields.Boolean(default=True) + company_id = fields.Many2one( + comodel_name="res.company", + default=lambda self: self.env.company, + help="Specify a company" + " if you want to define this Ecotax Classification only for specific" + " company. Otherwise, this Fiscal Classification will be available" + " for all companies.", + ) + product_status = fields.Selection( + [("M", "Menager"), ("P", "Professionnel")], + required=True, + ) + supplier_status = fields.Selection( + [ + ("MAN", "Manufacturer"), + ("RES", "Reseller, under their own brand"), + ("INT", "Introducer"), + ("IMP", "Importer"), + ("REM", "Remote vendor"), + ], + required=True, + help="MAN ==> Manufacturer: is locally established in the country, " + "and manufactures goods which are subject to ecotaxes\n" + "under their own name and brand, or designs such goods, " + "subcontracts the manufacturing and then sells them under " + "their own name and brand\n" + "RES ==> Reseller, under their own brand: is locally established " + "in the country, and sells under their own name or brand goods" + " subject to ecotax manufactured by others\n" + "INT ==> Introducer: is locally established and sells on the local " + "market goods subject to ecotax coming from other countries " + "of the European Union\n" + "IMP ==> Importer: is established in France, and sells on the local" + " market goods subject to ecotax coming from countries outside" + "the European Union\n" + "REM ==> Remote vendor: is established in another country of " + "the European Union or outside the EU, and remotely sells good " + "subject to ecotaxes to customers in the country", + ) + intrastat_code = fields.Char() + scale_code = fields.Char() + + @api.depends("ecotax_type") + def _compute_ecotax_vals(self): + for classif in self: + if classif.ecotax_type == "weight_based": + classif.default_fixed_ecotax = 0 + elif classif.ecotax_type == "fixed": + classif.ecotax_coef = 0 diff --git a/account_ecotax/models/account_move.py b/account_ecotax/models/account_move.py new file mode 100644 index 000000000..f30a74eb7 --- /dev/null +++ b/account_ecotax/models/account_move.py @@ -0,0 +1,61 @@ +# © 2014-2023 Akretion (http://www.akretion.com) +# @author Mourad EL HADJ MIMOUNE +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import _, api, fields, models +from odoo.tools.misc import formatLang + + +class AccountMove(models.Model): + _inherit = "account.move" + + amount_ecotax = fields.Float( + digits="Ecotax", + string="Included Ecotax", + store=True, + compute="_compute_ecotax", + ) + + @api.depends("invoice_line_ids.subtotal_ecotax") + def _compute_ecotax(self): + for move in self: + move.amount_ecotax = sum(move.line_ids.mapped("subtotal_ecotax")) + + @api.model + def _get_tax_totals( + self, partner, tax_lines_data, amount_total, amount_untaxed, currency + ): + """Include Ecotax when this method is called upon a single invoice + + NB: `_get_tax_totals()` is called when field `tax_totals_json` is + computed, which is used in invoice form view to display taxes and + totals. + """ + res = super()._get_tax_totals( + partner, tax_lines_data, amount_total, amount_untaxed, currency + ) + if len(self) != 1: + return res + + base_amt = self.amount_total + ecotax_amt = self.amount_ecotax + if not ecotax_amt: + return res + + env = self.with_context(lang=partner.lang).env + fmt_ecotax_amt = formatLang(env, ecotax_amt, currency_obj=currency) + fmt_base_amt = formatLang(env, base_amt, currency_obj=currency) + data = list(res["groups_by_subtotal"].get(_("Untaxed Amount")) or []) + data.append( + { + "tax_group_name": _("Included Ecotax"), + "tax_group_amount": ecotax_amt, + "formatted_tax_group_amount": fmt_ecotax_amt, + "tax_group_base_amount": base_amt, + "formatted_tax_group_base_amount": fmt_base_amt, + "tax_group_id": False, # Not an actual tax + "group_key": "Included Ecotax", + } + ) + res["groups_by_subtotal"][_("Untaxed Amount")] = data + return res diff --git a/account_ecotax/models/account_move_line.py b/account_ecotax/models/account_move_line.py new file mode 100644 index 000000000..77e33ca98 --- /dev/null +++ b/account_ecotax/models/account_move_line.py @@ -0,0 +1,70 @@ +# © 2014-2023 Akretion (http://www.akretion.com) +# @author Mourad EL HADJ MIMOUNE +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import Command, api, fields, models + + +class AcountMoveLine(models.Model): + _inherit = "account.move.line" + + ecotax_line_ids = fields.One2many( + "account.move.line.ecotax", + "account_move_line_id", + string="Ecotax lines", + copy=True, + ) + subtotal_ecotax = fields.Float( + string="Ecotax", digits="Ecotax", store=True, compute="_compute_ecotax" + ) + ecotax_amount_unit = fields.Float( + digits="Ecotax", + string="Ecotax Unit", + store=True, + compute="_compute_ecotax", + ) + + def _get_ecotax_amounts(self): + self.ensure_one() + amount_unit = sum(self.ecotax_line_ids.mapped("amount_unit")) + subtotal_ecotax = sum(self.ecotax_line_ids.mapped("amount_total")) + return amount_unit, subtotal_ecotax + + @api.depends( + "currency_id", + "ecotax_line_ids.amount_unit", + "ecotax_line_ids.amount_total", + ) + def _compute_ecotax(self): + for line in self: + amount_unit, amount_total = line._get_ecotax_amounts() + line.ecotax_amount_unit = amount_unit + line.subtotal_ecotax = amount_total + + @api.onchange("product_id") + def _onchange_product_ecotax_line(self): + """Unlink and recreate ecotax_lines when modifying the product_id.""" + self.ecotax_line_ids.unlink() # Remove all ecotax classification + if self.product_id: + self.ecotax_line_ids = [ + Command.create( + { + "classification_id": ecotaxline_prod.classification_id.id, + "force_amount_unit": ecotaxline_prod.force_amount, + } + ) + for ecotaxline_prod in self.product_id.all_ecotax_line_product_ids + ] + + def edit_ecotax_lines(self): + view = { + "name": ("Ecotax classification"), + "view_type": "form", + "view_mode": "form", + "res_model": "account.move.line", + "view_id": self.env.ref("account_ecotax.view_move_line_ecotax_form").id, + "type": "ir.actions.act_window", + "target": "new", + "res_id": self.id, + } + return view diff --git a/account_ecotax/models/account_move_line_ecotax.py b/account_ecotax/models/account_move_line_ecotax.py new file mode 100644 index 000000000..1372f0512 --- /dev/null +++ b/account_ecotax/models/account_move_line_ecotax.py @@ -0,0 +1,28 @@ +# © 2014-2023 Akretion (http://www.akretion.com) +# @author Mourad EL HADJ MIMOUNE +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class AccountMoveLineEcotax(models.Model): + _name = "account.move.line.ecotax" + _inherit = "ecotax.line.mixin" + _description = "Account move line ecotax" + + account_move_line_id = fields.Many2one( + comodel_name="account.move.line", + string="Account move line", + required=True, + readonly=True, + index=True, + auto_join=True, + ondelete="cascade", + ) + product_id = fields.Many2one( + "product.product", related="account_move_line_id.product_id", readonly=True + ) + quantity = fields.Float(related="account_move_line_id.quantity", readonly=True) + currency_id = fields.Many2one( + related="account_move_line_id.currency_id", readonly=True + ) diff --git a/account_ecotax/models/account_tax.py b/account_ecotax/models/account_tax.py new file mode 100644 index 000000000..fd592c692 --- /dev/null +++ b/account_ecotax/models/account_tax.py @@ -0,0 +1,32 @@ +# © 2014-2024 Akretion (http://www.akretion.com) +# @author Mourad EL HADJ MIMOUNE +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import api, fields, models + + +class AccountTax(models.Model): + _inherit = "account.tax" + + is_ecotax = fields.Boolean( + "Ecotax", + help="Warning : To include Ecotax " + "in the VAT tax check this :\n" + '1: check "included in base amount "\n' + "2: The Ecotax sequence must be less then " + "VAT tax (in sale and purchase)", + ) + + @api.onchange("is_ecotax") + def onchange_is_ecotax(self): + if self.is_ecotax: + self.amount_type = "code" + self.include_base_amount = True + self.python_compute = """ +# price_unit +# product: product.product object or None +# 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 + """ diff --git a/account_ecotax/models/ecotax_collector.py b/account_ecotax/models/ecotax_collector.py new file mode 100644 index 000000000..0aefb6072 --- /dev/null +++ b/account_ecotax/models/ecotax_collector.py @@ -0,0 +1,14 @@ +# Copyright 2021 Camptocamp +# @author Silvio Gregorini +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class EcotaxCollector(models.Model): + _name = "ecotax.collector" + _description = "Ecotax collector" + + name = fields.Char(required=True) + partner_id = fields.Many2one("res.partner", string="Partner", required=False) + active = fields.Boolean(default=True) diff --git a/account_ecotax/models/ecotax_line_mixin.py b/account_ecotax/models/ecotax_line_mixin.py new file mode 100644 index 000000000..785c8abc5 --- /dev/null +++ b/account_ecotax/models/ecotax_line_mixin.py @@ -0,0 +1,60 @@ +# © 2014-2023 Akretion (http://www.akretion.com) +# @author Mourad EL HADJ MIMOUNE +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import api, fields, models + + +class EcotaxLineMixin(models.AbstractModel): + """Mixin class for objects which can be used to save + multi ecotax classification by account move line + or sale order line.""" + + _name = "ecotax.line.mixin" + _description = "Ecotax Line Mixin" + + product_id = fields.Many2one("product.product", string="Product", readonly=True) + currency_id = fields.Many2one("res.currency", string="Currency") + classification_id = fields.Many2one( + "account.ecotax.classification", + string="Classification", + ) + amount_unit = fields.Float( + digits="Ecotax", + compute="_compute_ecotax", + help="Ecotax Amount computed from Classification or Manual ecotax", + store=True, + ) + force_amount_unit = fields.Float( + digits="Ecotax", + help="Force ecotax.\n" + "Allow to add a subtitle to the default Ecotax Classification", + ) + amount_total = fields.Float( + digits="Ecotax", + compute="_compute_ecotax", + help="Ecotax Amount total computed from Classification or forced ecotax amount", + store=True, + ) + quantity = fields.Float(digits="Product Unit of Measure", readonly=True) + + @api.depends( + "classification_id", + "force_amount_unit", + "product_id", + "quantity", + ) + def _compute_ecotax(self): + for ecotaxline in self: + ecotax_classif = ecotaxline.classification_id + + if ecotaxline.force_amount_unit: + # force ecotax amount + amt = ecotaxline.force_amount_unit + elif ecotax_classif.ecotax_type == "weight_based": + amt = ecotax_classif.ecotax_coef * (ecotaxline.product_id.weight or 0.0) + else: + amt = ecotax_classif.default_fixed_ecotax + + ecotaxline.amount_unit = amt + ecotaxline.amount_total = ecotaxline.amount_unit * ecotaxline.quantity diff --git a/account_ecotax/models/ecotax_line_product.py b/account_ecotax/models/ecotax_line_product.py new file mode 100644 index 000000000..055715750 --- /dev/null +++ b/account_ecotax/models/ecotax_line_product.py @@ -0,0 +1,82 @@ +# © 2014-2023 Akretion (http://www.akretion.com) +# @author Mourad EL HADJ MIMOUNE +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import api, fields, models + + +class EcotaxLineProduct(models.Model): + """class for objects which can be used to save mutiple ecotax classifications + by product.""" + + _name = "ecotax.line.product" + _description = "Ecotax Line product" + + product_tmpl_id = fields.Many2one( + "product.template", string="Product Template", readonly=True + ) + product_id = fields.Many2one("product.product", string="Product", readonly=True) + currency_id = fields.Many2one(related="product_tmpl_id.currency_id", readonly=True) + classification_id = fields.Many2one( + "account.ecotax.classification", + string="Classification", + ) + force_amount = fields.Float( + digits="Ecotax", + help="Force ecotax amount.\n" + "Allow to substitute default Ecotax Classification", + ) + amount = fields.Float( + digits="Ecotax", + compute="_compute_ecotax", + help="Ecotax Amount computed form Classification or forced ecotax amount", + store=True, + ) + display_name = fields.Char(compute="_compute_display_name") + + @api.depends("classification_id", "amount") + def _compute_display_name(self): + for rec in self: + rec.display_name = "%s (%s)" % ( + rec.classification_id.name, + rec.amount, + ) + + @api.depends( + "classification_id", + "classification_id.ecotax_type", + "classification_id.ecotax_coef", + "product_tmpl_id", + "product_tmpl_id.weight", + "product_id", + "force_amount", + ) + def _compute_ecotax(self): + for ecotaxline in self: + ecotax_cls = ecotaxline.classification_id + + if ecotax_cls.ecotax_type == "weight_based": + amt = ecotax_cls.ecotax_coef * ( + ecotaxline.product_tmpl_id.weight + or ecotaxline.product_id.weight + or 0.0 + ) + else: + amt = ecotax_cls.default_fixed_ecotax + # force ecotax amount + if ecotaxline.force_amount: + amt = ecotaxline.force_amount + ecotaxline.amount = amt + + _sql_constraints = [ + ( + "unique_classification_id_by_product", + "UNIQUE(classification_id, product_id)", + "Only one ecotax classification occurrence by product", + ), + ( + "unique_classification_id_by_product_tmpl", + "UNIQUE(classification_id, product_tmpl_id)", + "Only one ecotax classification occurrence by product Template", + ), + ] diff --git a/account_ecotax/models/ecotax_sector.py b/account_ecotax/models/ecotax_sector.py new file mode 100644 index 000000000..8bc47d3fa --- /dev/null +++ b/account_ecotax/models/ecotax_sector.py @@ -0,0 +1,14 @@ +# Copyright 2021 Camptocamp +# @author Silvio Gregorini +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class EcotaxSector(models.Model): + _name = "ecotax.sector" + _description = "Ecotax Sector" + + name = fields.Char(required=True) + description = fields.Char() + active = fields.Boolean(default=True) diff --git a/account_ecotax/models/product_product.py b/account_ecotax/models/product_product.py new file mode 100644 index 000000000..fa108141e --- /dev/null +++ b/account_ecotax/models/product_product.py @@ -0,0 +1,90 @@ +# Copyright 2021 Camptocamp +# @author Silvio Gregorini +# Copyright 2023 Akretion (http://www.akretion.com) +# # @author Mourad EL HADJ MIMOUNE +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import api, fields, models +from odoo.osv import expression + + +class ProductProduct(models.Model): + _inherit = "product.product" + + additional_ecotax_line_product_ids = fields.One2many( + "ecotax.line.product", + "product_id", + string="Additional ecotax lines", + copy=True, + domain="[('id', 'not in', ecotax_line_product_ids)]", + ) + all_ecotax_line_product_ids = fields.One2many( + "ecotax.line.product", + compute="_compute_all_ecotax_line_product_ids", + search="_search_all_ecotax_line_product_ids", + string="All ecotax lines", + help="Contain all ecotaxs classification defined in product template" + "and the additionnal.\n" + "ecotaxs defined in product variant. For more details" + "see the product variant accounting tab", + ) + ecotax_amount = fields.Float( + digits="Ecotax", + compute="_compute_product_ecotax", + store=True, + help="Ecotax Amount computed form all ecotax line classification", + ) + fixed_ecotax = fields.Float( + compute="_compute_product_ecotax", + store=True, + help="Fixed ecotax of the Ecotax Classification", + ) + weight_based_ecotax = fields.Float( + compute="_compute_product_ecotax", + store=True, + help="Ecotax value :\n" "product weight * ecotax coef of Ecotax Classification", + ) + + @api.depends("ecotax_line_product_ids", "additional_ecotax_line_product_ids") + def _compute_all_ecotax_line_product_ids(self): + for product in self: + product.all_ecotax_line_product_ids = ( + product.ecotax_line_product_ids + | product.additional_ecotax_line_product_ids + ) + + def _search_all_ecotax_line_product_ids(self, operator, operand): + if operator in expression.NEGATIVE_TERM_OPERATORS: + return [ + ("ecotax_line_product_ids", operator, operand), + ("additional_ecotax_line_product_ids", operator, operand), + ] + return [ + "|", + ("ecotax_line_product_ids", operator, operand), + ("additional_ecotax_line_product_ids", operator, operand), + ] + + @api.depends( + "all_ecotax_line_product_ids", + "all_ecotax_line_product_ids.classification_id", + "all_ecotax_line_product_ids.classification_id.ecotax_type", + "all_ecotax_line_product_ids.classification_id.ecotax_coef", + "all_ecotax_line_product_ids.force_amount", + "weight", + ) + 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 + product.fixed_ecotax = fixed_ecotax + product.weight_based_ecotax = weight_based_ecotax + product.ecotax_amount = amount_ecotax diff --git a/account_ecotax/models/product_template.py b/account_ecotax/models/product_template.py new file mode 100644 index 000000000..7525e87ef --- /dev/null +++ b/account_ecotax/models/product_template.py @@ -0,0 +1,56 @@ +# © 2014-2023 Akretion (http://www.akretion.com) +# @author Mourad EL HADJ MIMOUNE +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import api, fields, models + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + ecotax_line_product_ids = fields.One2many( + "ecotax.line.product", + "product_tmpl_id", + string="Ecotax lines", + copy=True, + ) + ecotax_amount = fields.Float( + digits="Ecotax", + compute="_compute_ecotax", + help="Ecotax Amount computed from Classification", + store=True, + ) + fixed_ecotax = fields.Float( + compute="_compute_ecotax", + help="Fixed ecotax of the Ecotax Classification", + store=True, + ) + weight_based_ecotax = fields.Float( + compute="_compute_ecotax", + help="Ecotax value :\nproduct weight * ecotax coef of Ecotax Classification", + store=True, + ) + + @api.depends( + "ecotax_line_product_ids", + "ecotax_line_product_ids.classification_id", + "ecotax_line_product_ids.classification_id.ecotax_type", + "ecotax_line_product_ids.classification_id.ecotax_coef", + "ecotax_line_product_ids.force_amount", + "weight", + ) + def _compute_ecotax(self): + for tmpl in self: + amount_ecotax = 0.0 + weight_based_ecotax = 0.0 + fixed_ecotax = 0.0 + for ecotaxline_prod in tmpl.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 + tmpl.fixed_ecotax = fixed_ecotax + tmpl.weight_based_ecotax = weight_based_ecotax + tmpl.ecotax_amount = amount_ecotax diff --git a/account_ecotax/readme/CONTRIBUTORS.rst b/account_ecotax/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..bfb2a5c68 --- /dev/null +++ b/account_ecotax/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Mourad EL HADJ MIMOUNE +* Florian da Costa diff --git a/account_ecotax/readme/DESCRIPTION.rst b/account_ecotax/readme/DESCRIPTION.rst new file mode 100644 index 000000000..4e45a942f --- /dev/null +++ b/account_ecotax/readme/DESCRIPTION.rst @@ -0,0 +1,39 @@ +This module applies to companies based in France mainland. It doesn't apply to +companies based in the DOM-TOMs (Guadeloupe, Martinique, Guyane, Réunion, +Mayotte). + +It add Ecotaxe amount on invoice line. +furthermore, a total ecotaxe are added at the footer of each document. + +To make easy ecotaxe management and to factor the data, ecotaxe are set on products via ECOTAXE classifications. +ECOTAXE classification can either a fixed or weight based ecotaxe. + +A product can have one or serveral ecotaxe classifications. For exemple wooden window blinds equipped with electric motor can +have ecotaxe for wood and ecotaxe for electric motor. + +This module has some limits : +- The ecotax amount is always included in the price of the product. +- The ecotax amount is not isolated in an specific accounting account but is included in the product income account. + +If one of these limits is an issue, you could install the submodule account_ecotax_tax. +This second module lets you manage the ecotax as a tax, so you can configure if you want it to be included or excluded of product price and also configuring an accounting account to isolate it. +The main consequence of this approach is that the ecotax won't be considered in the turnover, since it is considered as a tax. + +This module version add the possibility to manage several ecotaxe classification by product. +A migration script is necessary to update from previous versions. + +There is the main change to manage in migration script: + +renamed field +model old field new field +account.move.line unit_ecotaxe_amount ecotaxe_amount_unit +product.template manual_fixed_ecotaxe force_ecotaxe_amount + +changed fields +model old field new field +product.template ecotaxe_classification_id ecotaxe_classification_ids + +added fields +model new field +account.move.line ecotaxe_line_ids +product.template ecotaxe_line_product_ids diff --git a/account_ecotax/readme/USAGE.rst b/account_ecotax/readme/USAGE.rst new file mode 100644 index 000000000..b9d427336 --- /dev/null +++ b/account_ecotax/readme/USAGE.rst @@ -0,0 +1,12 @@ +1. Add an ecotax classification via the menu **Accounting > Configuration > Taxes > Ecotax Classification**. + + - The ecotax classification can be either a fixed ecotax or a weight-based ecotax. + - Ecotax classification information can be used for legal declarations. + - For the fixed ecotax, the ecotax amount is used as a default value, which can be overridden on the product. + - For the weight-based ecotax, define one ecotax by a coefficient applied to the weight (depending on the product's materials). + +2. Assign one or more ecotax classifications to a product. + + - The ecotax amount can also be manually overridden on the product. + +3. Create an invoice with this product diff --git a/account_ecotax/security/ir.model.access.csv b/account_ecotax/security/ir.model.access.csv new file mode 100644 index 000000000..1d9afe5a1 --- /dev/null +++ b/account_ecotax/security/ir.model.access.csv @@ -0,0 +1,13 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +ir_model_access_account_ecotax_classification_manager,Full access account_ecotax_classification to accountant,model_account_ecotax_classification,account.group_account_manager,1,1,1,1 +ir_model_access_account_ecotax_category_manager,Full access to account.ecotax.category to accoutant,model_account_ecotax_category,account.group_account_manager,1,1,1,1 +ir_model_access_ecotax_sector_manager,Full access to ecotax.sector to accoutant,model_ecotax_sector,account.group_account_manager,1,1,1,1 +ir_model_access_ecotax_collector_manager,Full access to ecotax.collector to accoutant,model_ecotax_collector,account.group_account_manager,1,1,1,1 +ir_model_access_account_ecotax_classification_user,Read access account_ecotax_classification to group invoice,model_account_ecotax_classification,account.group_account_invoice,1,0,0,0 +ir_model_access_account_ecotax_category_user,Read access to account.ecotax.category to group invoice,model_account_ecotax_category,account.group_account_invoice,1,0,0,0 +ir_model_access_ecotax_sector_user,Read access to ecotax.sector to group invoice,model_ecotax_sector,account.group_account_invoice,1,0,0,0 +ir_model_access_ecotax_collector_user,Read access to ecotax.collector to group invoice,model_ecotax_collector,account.group_account_invoice,1,0,0,0 +access_account_move_line_ecotax_readonly,account.move.line.ecotax readonly,model_account_move_line_ecotax,account.group_account_readonly,1,0,0,0 +ir_model_access_account_move_line_ecotax_group_invoice,Read Full acess to model_account_move_line_ecotax to group invoice,model_account_move_line_ecotax,account.group_account_invoice,1,1,1,1 +ir_model_access_ecotax_line_product_group_invoice,Read Full acess to model_ecotax_line_product to group invoice,model_ecotax_line_product,account.group_account_invoice,1,1,1,1 +access_ecotax_line_product_readonly,ecotax.line.product readonly,model_ecotax_line_product,account.group_account_readonly,1,0,0,0 diff --git a/account_ecotax/security/ir_rule.xml b/account_ecotax/security/ir_rule.xml new file mode 100644 index 000000000..9c1b55310 --- /dev/null +++ b/account_ecotax/security/ir_rule.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + Ecotax Classification + + + + ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)] + + + diff --git a/account_ecotax/static/description/icon.png b/account_ecotax/static/description/icon.png new file mode 100644 index 000000000..ee185a69f Binary files /dev/null and b/account_ecotax/static/description/icon.png differ diff --git a/account_ecotax/static/description/index.html b/account_ecotax/static/description/index.html new file mode 100644 index 000000000..03e212e06 --- /dev/null +++ b/account_ecotax/static/description/index.html @@ -0,0 +1,472 @@ + + + + + +Ecotax Management + + + +
+

Ecotax Management

+ + +

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

+

This module applies to companies based in France mainland. It doesn’t apply to +companies based in the DOM-TOMs (Guadeloupe, Martinique, Guyane, Réunion, +Mayotte).

+

It add Ecotaxe amount on invoice line. +furthermore, a total ecotaxe are added at the footer of each document.

+

To make easy ecotaxe management and to factor the data, ecotaxe are set on products via ECOTAXE classifications. +ECOTAXE classification can either a fixed or weight based ecotaxe.

+

A product can have one or serveral ecotaxe classifications. For exemple wooden window blinds equipped with electric motor can +have ecotaxe for wood and ecotaxe for electric motor.

+

This module has some limits : +- The ecotax amount is always included in the price of the product. +- The ecotax amount is not isolated in an specific accounting account but is included in the product income account.

+

If one of these limits is an issue, you could install the submodule account_ecotax_tax. +This second module lets you manage the ecotax as a tax, so you can configure if you want it to be included or excluded of product price and also configuring an accounting account to isolate it. +The main consequence of this approach is that the ecotax won’t be considered in the turnover, since it is considered as a tax.

+

This module version add the possibility to manage several ecotaxe classification by product. +A migration script is necessary to update from previous versions.

+

There is the main change to manage in migration script:

+

renamed field +model old field new field +account.move.line unit_ecotaxe_amount ecotaxe_amount_unit +product.template manual_fixed_ecotaxe force_ecotaxe_amount

+

changed fields +model old field new field +product.template ecotaxe_classification_id ecotaxe_classification_ids

+

added fields +model new field +account.move.line ecotaxe_line_ids +product.template ecotaxe_line_product_ids

+

Table of contents

+ +
+

Usage

+
    +
  1. Add an ecotax classification via the menu Accounting > Configuration > Taxes > Ecotax Classification.
      +
    • The ecotax classification can be either a fixed ecotax or a weight-based ecotax.
    • +
    • Ecotax classification information can be used for legal declarations.
    • +
    • For the fixed ecotax, the ecotax amount is used as a default value, which can be overridden on the product.
    • +
    • For the weight-based ecotax, define one ecotax by a coefficient applied to the weight (depending on the product’s materials).
    • +
    +
  2. +
  3. Assign one or more ecotax classifications to a product.
      +
    • The ecotax amount can also be manually overridden on the product.
    • +
    +
  4. +
  5. Create an invoice with this product
  6. +
+
+
+

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 +feedback.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • Akretion
  • +
+
+
+

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.

+

Current maintainers:

+

mourad-ehm florian-dacosta

+

This module is part of the OCA/account-fiscal-rule project on GitHub.

+

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

+
+
+
+ + diff --git a/account_ecotax/tests/__init__.py b/account_ecotax/tests/__init__.py new file mode 100644 index 000000000..88a931bae --- /dev/null +++ b/account_ecotax/tests/__init__.py @@ -0,0 +1 @@ +from . import test_ecotax diff --git a/account_ecotax/tests/test_ecotax.py b/account_ecotax/tests/test_ecotax.py new file mode 100644 index 000000000..82e24ac3f --- /dev/null +++ b/account_ecotax/tests/test_ecotax.py @@ -0,0 +1,423 @@ +# Copyright 2016-2023 Akretion France +# @author: Alexis de Lattre +# Copyright 2021 Camptocamp +# @author Silvio Gregorini +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from random import choice + +from odoo import Command +from odoo.tests.common import Form, TransactionCase + + +class TestInvoiceEcotaxCommon(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + + # ACCOUNTING STUFF + # 1- Tax account + cls.invoice_tax_account = cls.env["account.account"].create( + { + "code": "47590", + "name": "Invoice Tax Account", + "account_type": "liability_current", + "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, + }, + ), + ], + } + ) + # ECOTAXES + # 1- Fixed ecotax + cls.ecotax_fixed = cls.env["account.ecotax.classification"].create( + { + "name": "Fixed Ecotax", + "ecotax_type": "fixed", + "default_fixed_ecotax": 5.0, + "product_status": "M", + "supplier_status": "MAN", + } + ) + # 2- Weight-based ecotax + cls.ecotax_weight = cls.env["account.ecotax.classification"].create( + { + "name": "Weight Based Ecotax", + "ecotax_type": "weight_based", + "ecotax_coef": 0.04, + "product_status": "P", + "supplier_status": "MAN", + } + ) + # MISC + # 1- Invoice partner + cls.invoice_partner = cls.env["res.partner"].create({"name": "Test"}) + + @classmethod + def _make_invoice(cls, products): + """Creates a new customer invoice with given products and returns it""" + move_form = Form( + cls.env["account.move"].with_context(default_move_type="out_invoice") + ) + move_form.partner_id = cls.invoice_partner + + for product in products or []: + with move_form.invoice_line_ids.new() as line_form: + line_form.product_id = product + + invoice = move_form.save() + return invoice + + @classmethod + def _make_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]), + "ecotax_line_product_ids": [ + ( + 0, + 0, + { + "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, + "taxes_id": [Command.set(cls.invoice_tax.ids)], + } + ) + return tmpl.product_variant_ids[0] + + @classmethod + def _make_product_variants(cls, ecotax_classification): + """Creates a product variants with given ecotax classification + Returns the newly created template variants + """ + size_attr = cls.env["product.attribute"].create( + { + "name": "Size", + "create_variant": "always", + "value_ids": [(0, 0, {"name": "S"}), (0, 0, {"name": "M"})], + } + ) + + tmpl = cls.env["product.template"].create( + { + "name": " - ".join(["Product", ecotax_classification.name]), + "ecotax_line_product_ids": [ + ( + 0, + 0, + { + "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, + "attribute_line_ids": [ + ( + 0, + 0, + { + "attribute_id": size_attr.id, + "value_ids": [(6, 0, size_attr.value_ids.ids)], + }, + ) + ], + } + ) + return tmpl.product_variant_ids + + @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 = [] + 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([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + 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_ecotax, inv_expected_amounts["amount_ecotax"]) + 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.ecotax_amount_unit, + inv_line_expected_amounts["ecotax_amount_unit"], + ) + self.assertEqual( + inv_line.subtotal_ecotax, inv_line_expected_amounts["subtotal_ecotax"] + ) + + 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._make_invoice(products=self._make_product(self.ecotax_fixed)) + self._run_checks( + invoice, + {"amount_ecotax": 5.0, "amount_total": 100.0}, + [{"ecotax_amount_unit": 5.0, "subtotal_ecotax": 5.0}], + ) + new_qty = self._set_invoice_lines_random_quantities(invoice)[0] + self._run_checks( + invoice, + {"amount_ecotax": 5.0 * new_qty, "amount_total": 100.0 * new_qty}, + [{"ecotax_amount_unit": 5.0, "subtotal_ecotax": 5.0 * new_qty}], + ) + + def _test_02_force_fixed_ecotax_on_product(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 + - Force ecotax 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._make_product(self.ecotax_fixed) + product.ecotax_line_product_ids[0].force_amount = 10 + invoice = self._make_invoice(products=product) + self._run_checks( + invoice, + {"amount_ecotax": 10.0, "amount_total": 100.0}, + [{"ecotax_amount_unit": 10.0, "subtotal_ecotax": 10.0}], + ) + new_qty = self._set_invoice_lines_random_quantities(invoice)[0] + self._run_checks( + invoice, + {"amount_ecotax": 10.0 * new_qty, "amount_total": 100.0 * new_qty}, + [{"ecotax_amount_unit": 10.0, "subtotal_ecotax": 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._make_invoice(products=self._make_product(self.ecotax_weight)) + self._run_checks( + invoice, + {"amount_ecotax": 4.0, "amount_total": 100.0}, + [{"ecotax_amount_unit": 4.0, "subtotal_ecotax": 4.0}], + ) + new_qty = self._set_invoice_lines_random_quantities(invoice)[0] + self._run_checks( + invoice, + {"amount_ecotax": 4.0 * new_qty, "amount_total": 100.0 * new_qty}, + [{"ecotax_amount_unit": 4.0, "subtotal_ecotax": 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._make_product(self.ecotax_fixed) + manual_fixed_product = self._make_product(self.ecotax_fixed) + manual_fixed_product.ecotax_line_product_ids[0].force_amount = 10 + weight_based_product = self._make_product(self.ecotax_weight) + invoice = self._make_invoice( + products=default_fixed_product | manual_fixed_product | weight_based_product + ) + self._run_checks( + invoice, + {"amount_ecotax": 19.0, "amount_total": 300.0}, + [ + {"ecotax_amount_unit": 5.0, "subtotal_ecotax": 5.0}, + {"ecotax_amount_unit": 10.0, "subtotal_ecotax": 10.0}, + {"ecotax_amount_unit": 4.0, "subtotal_ecotax": 4.0}, + ], + ) + new_qtys = self._set_invoice_lines_random_quantities(invoice) + self._run_checks( + invoice, + { + "amount_ecotax": 5.0 * new_qtys[0] + + 10.0 * new_qtys[1] + + 4.0 * new_qtys[2], + "amount_total": 100.0 * sum(new_qtys), + }, + [ + {"ecotax_amount_unit": 5.0, "subtotal_ecotax": 5.0 * new_qtys[0]}, + {"ecotax_amount_unit": 10.0, "subtotal_ecotax": 10.0 * new_qtys[1]}, + {"ecotax_amount_unit": 4.0, "subtotal_ecotax": 4.0 * new_qtys[2]}, + ], + ) + + def _test_05_product_variants(self): + """ + Data: + A product template with two variants + Test Case: + Add additional ecotax line to one variant + Expected result: + The additional ecotax line is not associated to second variant + the all ecotax lines of the variant contains both the ecotax + line of the product template and the additional ecotax line + """ + variants = self._make_product_variants(self.ecotax_fixed) + self.assertEqual(len(variants), 2) + variant_1 = variants[0] + variant_2 = variants[1] + self.assertEqual( + variant_1.all_ecotax_line_product_ids, + variant_2.all_ecotax_line_product_ids, + ) + variant_1.additional_ecotax_line_product_ids = [ + ( + 0, + 0, + { + "classification_id": self.ecotax_weight.id, + }, + ) + ] + all_additional_ecotax = ( + variant_1.additional_ecotax_line_product_ids + | variant_1.product_tmpl_id.ecotax_line_product_ids + ) + self.assertEqual( + len(variant_1.all_ecotax_line_product_ids), + 2, + ) + self.assertEqual( + len(variant_2.all_ecotax_line_product_ids), + 1, + ) + self.assertEqual( + variant_1.all_ecotax_line_product_ids, + all_additional_ecotax, + ) + self.assertEqual( + variant_2.all_ecotax_line_product_ids, + variant_2.product_tmpl_id.ecotax_line_product_ids, + ) + + +class TestInvoiceEcotax(TestInvoiceEcotaxCommon): + def test_01_default_fixed_ecotax(self): + self._test_01_default_fixed_ecotax() + + def test_02_force_fixed_ecotax_on_product(self): + self._test_02_force_fixed_ecotax_on_product() + + def test_03_weight_based_ecotax(self): + self._test_03_weight_based_ecotax() + + def test_04_mixed_ecotax(self): + self._test_04_mixed_ecotax() + + def test_05_product_variants(self): + self._test_05_product_variants() diff --git a/account_ecotax/views/account_ecotax_category_view.xml b/account_ecotax/views/account_ecotax_category_view.xml new file mode 100644 index 000000000..d100c7623 --- /dev/null +++ b/account_ecotax/views/account_ecotax_category_view.xml @@ -0,0 +1,72 @@ + + + + + account.ecotax.category + + + + + + + + + + account.ecotax.category + +
+ + +
+
+
+

+ +

+
+ + + + +
+
+
+
+ + account.ecotax.category + + + + + + + + Ecotaxe category + account.ecotax.category + tree,form + +

+ Click to start a new Ecotaxe category. +

+
+
+ +
diff --git a/account_ecotax/views/account_ecotax_classification_view.xml b/account_ecotax/views/account_ecotax_classification_view.xml new file mode 100644 index 000000000..21fb08175 --- /dev/null +++ b/account_ecotax/views/account_ecotax_classification_view.xml @@ -0,0 +1,123 @@ + + + + + account.ecotax.classification + + + + + + + + + + + + + + + account.ecotax.classification + +
+ + +
+
+
+

+ +

+
+ + + + + + + + + + + + + + + + + + +
+
+
+
+ + account.ecotax.classification + + + + + + + + + + + + + Ecotaxe Classification + account.ecotax.classification + tree,form + +

+ Click to start a new Ecotaxe Classification. +

+
+
+ +
diff --git a/account_ecotax/views/account_move_view.xml b/account_ecotax/views/account_move_view.xml new file mode 100644 index 000000000..61d8f0aa2 --- /dev/null +++ b/account_ecotax/views/account_move_view.xml @@ -0,0 +1,170 @@ + + + + + account.move + + + + + +