From f4d842b8a8daed91bab3f290918cc781ee121c67 Mon Sep 17 00:00:00 2001 From: Mourad Date: Thu, 23 May 2024 17:24:07 +0200 Subject: [PATCH 01/15] [16.0] add account_ecotax module --- account_ecotax/README.rst | 120 +++++ account_ecotax/__init__.py | 1 + account_ecotax/__manifest__.py | 31 ++ account_ecotax/data/decimal_precision.xml | 7 + account_ecotax/models/__init__.py | 12 + .../models/account_ecotax_category.py | 15 + .../models/account_ecotax_classification.py | 109 +++++ account_ecotax/models/account_move.py | 61 +++ account_ecotax/models/account_move_line.py | 126 +++++ .../models/account_move_line_ecotax.py | 28 ++ account_ecotax/models/account_tax.py | 32 ++ account_ecotax/models/ecotax_collector.py | 14 + account_ecotax/models/ecotax_line_mixin.py | 59 +++ account_ecotax/models/ecotax_line_product.py | 81 ++++ account_ecotax/models/ecotax_sector.py | 14 + account_ecotax/models/product_product.py | 91 ++++ account_ecotax/models/product_template.py | 57 +++ account_ecotax/readme/CONTRIBUTORS.rst | 1 + account_ecotax/readme/DESCRIPTION.rst | 31 ++ account_ecotax/readme/USAGE.rst | 10 + account_ecotax/security/ir.model.access.csv | 13 + account_ecotax/security/ir_rule.xml | 18 + account_ecotax/static/description/icon.png | Bin 0 -> 3731 bytes account_ecotax/static/description/index.html | 453 ++++++++++++++++++ account_ecotax/tests/__init__.py | 1 + account_ecotax/tests/test_ecotax.py | 437 +++++++++++++++++ .../views/account_ecotax_category_view.xml | 72 +++ .../account_ecotax_classification_view.xml | 138 ++++++ account_ecotax/views/account_move_view.xml | 176 +++++++ account_ecotax/views/account_tax_view.xml | 18 + .../views/ecotax_collector_view.xml | 72 +++ account_ecotax/views/ecotax_sector_view.xml | 70 +++ .../views/product_template_view.xml | 33 ++ account_ecotax/views/product_view.xml | 94 ++++ .../account_ecotax/odoo/addons/account_ecotax | 1 + setup/account_ecotax/setup.py | 6 + 36 files changed, 2502 insertions(+) create mode 100644 account_ecotax/README.rst create mode 100644 account_ecotax/__init__.py create mode 100644 account_ecotax/__manifest__.py create mode 100644 account_ecotax/data/decimal_precision.xml create mode 100644 account_ecotax/models/__init__.py create mode 100644 account_ecotax/models/account_ecotax_category.py create mode 100644 account_ecotax/models/account_ecotax_classification.py create mode 100644 account_ecotax/models/account_move.py create mode 100644 account_ecotax/models/account_move_line.py create mode 100644 account_ecotax/models/account_move_line_ecotax.py create mode 100644 account_ecotax/models/account_tax.py create mode 100644 account_ecotax/models/ecotax_collector.py create mode 100644 account_ecotax/models/ecotax_line_mixin.py create mode 100644 account_ecotax/models/ecotax_line_product.py create mode 100644 account_ecotax/models/ecotax_sector.py create mode 100644 account_ecotax/models/product_product.py create mode 100644 account_ecotax/models/product_template.py create mode 100644 account_ecotax/readme/CONTRIBUTORS.rst create mode 100644 account_ecotax/readme/DESCRIPTION.rst create mode 100644 account_ecotax/readme/USAGE.rst create mode 100644 account_ecotax/security/ir.model.access.csv create mode 100644 account_ecotax/security/ir_rule.xml create mode 100644 account_ecotax/static/description/icon.png create mode 100644 account_ecotax/static/description/index.html create mode 100644 account_ecotax/tests/__init__.py create mode 100644 account_ecotax/tests/test_ecotax.py create mode 100644 account_ecotax/views/account_ecotax_category_view.xml create mode 100644 account_ecotax/views/account_ecotax_classification_view.xml create mode 100644 account_ecotax/views/account_move_view.xml create mode 100644 account_ecotax/views/account_tax_view.xml create mode 100644 account_ecotax/views/ecotax_collector_view.xml create mode 100644 account_ecotax/views/ecotax_sector_view.xml create mode 100644 account_ecotax/views/product_template_view.xml create mode 100644 account_ecotax/views/product_view.xml create mode 120000 setup/account_ecotax/odoo/addons/account_ecotax create mode 100644 setup/account_ecotax/setup.py diff --git a/account_ecotax/README.rst b/account_ecotax/README.rst new file mode 100644 index 000000000..93f5db377 --- /dev/null +++ b/account_ecotax/README.rst @@ -0,0 +1,120 @@ +================= +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 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 +===== + +Add ecotaxe classification via the menu *Accounting > configuration > Taxes > Ecotaxe Classification*. +Ecotaxe classification is either a fixed ecotaxe or weight based ecotaxe. +ecotaxe classification Infos can be used for legal declarations. +For fixed ecotaxe, ecotaxe amount is used as default value. We can for ecotaxe amount on product. + +For weight based ecotaxe, we should define one ecotaxe by coef applied for the weight (depending on product materials). + +Assign one or more ecotaxe classification to a product. + +we can also force amount ecotaxe on account move line by classification. + +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 + +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/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..3c3981ae0 --- /dev/null +++ b/account_ecotax/__manifest__.py @@ -0,0 +1,31 @@ +# © 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.2.0.0", + "author": "Akretion, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/account-fiscal-rule", + "category": "Localization/Account Taxes", + "license": "AGPL-3", + "depends": [ + "account", + "account_tax_python", + ], + "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", + "views/account_tax_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..6570d4f91 --- /dev/null +++ b/account_ecotax/models/account_ecotax_classification.py @@ -0,0 +1,109 @@ +# © 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" + + @api.model + def _default_company_id(self): + return self.env.company + + 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=_default_company_id, + 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( + [ + ("FAB", "Fabricant"), + ("REV", "Revendeur sous sa marque"), + ("INT", "Introducteur"), + ("IMP", "Importateur"), + ("DIS", "Vendeur à distance"), + ], + required=True, + help="FAB ==> Fabricant : est établi en France et fabrique des EEE\n" + "sous son propre nom ou sa propre marque, ou fait concevoir ou\n" + " fabriquer des EEE et les commercialise sous\n" + " son propre nom et sa propre marque\n" + "REV ==> Revendeur sous sa marque : est établi en France et vend,\n" + " sous son propre nom ou sa propre marque des EEE produits\n" + " par d'autres fournisseurs" + "INT ==> Introducteur : est établi en France et met sur le marché\n" + "des EEE provenant d'un autre Etat membre" + "IMP ==> Importateur : est établi en France et met sur marché\n" + "des EEE provenant de pays hors Union Européenne" + "DIS ==> Vendeur à distance : est établie dans un autre Etat\n" + "membre ou dans un pays tiers et vend en France des EEE par\n" + "communication à distance", + ) + emebi_code = fields.Char() + scale_code = fields.Char() + sale_ecotax_ids = fields.Many2many( + "account.tax", + "ecotax_classif_taxes_rel", + "ecotax_classif_id", + "tax_id", + string="Sale EcoTaxe", + domain=[("is_ecotax", "=", True), ("type_tax_use", "=", "sale")], + ) + purchase_ecotax_ids = fields.Many2many( + "account.tax", + "ecotax_classif_purchase_taxes_rel", + "ecotax_classif_id", + "tax_id", + string="Purchase EcoTaxe", + domain=[("is_ecotax", "=", True), ("type_tax_use", "=", "purchase")], + ) + + @api.depends("ecotax_type") + def _compute_ecotax_vals(self): + for classif in self: + if classif.ecotax_type == "weight_based": + classif.default_fixed_ecotax = 0 + if 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..9df0d5274 --- /dev/null +++ b/account_ecotax/models/account_move_line.py @@ -0,0 +1,126 @@ +# © 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 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( + digits="Ecotax", store=True, compute="_compute_ecotax" + ) + ecotax_amount_unit = fields.Float( + digits="Ecotax", + string="Ecotax Unit.", + store=True, + compute="_compute_ecotax", + ) + + @api.depends( + "move_id.currency_id", + "ecotax_line_ids", + "ecotax_line_ids.amount_unit", + "ecotax_line_ids.amount_total", + ) + def _compute_ecotax(self): + for line in self: + ecotax_ids = line.tax_ids.filtered(lambda tax: tax.is_ecotax) + + if line.display_type == "tax" or not ecotax_ids: + continue + if line.display_type == "product" and line.move_id.is_invoice(True): + amount_currency = line.price_unit * (1 - line.discount / 100) + handle_price_include = True + quantity = line.quantity + else: + amount_currency = line.amount_currency + handle_price_include = False + quantity = 1 + compute_all_currency = ecotax_ids.compute_all( + amount_currency, + currency=line.currency_id, + quantity=quantity, + product=line.product_id, + partner=line.move_id.partner_id or line.partner_id, + is_refund=line.is_refund, + handle_price_include=handle_price_include, + include_caba_tags=line.move_id.always_tax_exigible, + ) + subtotal_ecotax = 0.0 + for tax in compute_all_currency["taxes"]: + subtotal_ecotax += tax["amount"] + + unit = quantity and subtotal_ecotax / quantity or subtotal_ecotax + line.ecotax_amount_unit = unit + line.subtotal_ecotax = subtotal_ecotax + + @api.onchange("product_id") + def _onchange_product_ecotax_line(self): + """Unlink and recreate ecotax_lines when modifying the product_id.""" + if self.product_id: + self.ecotax_line_ids = [(5,)] # Remove all ecotax classification + ecotax_cls_vals = [] + for ecotaxline_prod in self.product_id.all_ecotax_line_product_ids: + classif_id = ecotaxline_prod.classification_id.id + forced_amount = ecotaxline_prod.force_amount + ecotax_cls_vals.append( + ( + 0, + 0, + { + "classification_id": classif_id, + "force_amount_unit": forced_amount, + }, + ) + ) + self.ecotax_line_ids = ecotax_cls_vals + else: + self.ecotax_line_ids = [(5,)] # Remove all ecotax classification + + 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 + + def _get_computed_taxes(self): + tax_ids = super()._get_computed_taxes() + 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") + 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") + ecotax_ids = purchase_ecotaxs.filtered( + lambda tax: tax.company_id == self.move_id.company_id + ) + + if ecotax_ids and self.move_id.fiscal_position_id: + ecotax_ids = self.move_id.fiscal_position_id.map_tax(ecotax_ids) + if ecotax_ids: + tax_ids |= ecotax_ids + + return tax_ids 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..d0e84dbf6 --- /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 = product.weight_based_ecotax or 0.0 +result = product.ecotax_amount 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..b86a353e2 --- /dev/null +++ b/account_ecotax/models/ecotax_line_mixin.py @@ -0,0 +1,59 @@ +# © 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 calssification 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 form Classification or Manuel ecotax", + store=True, + ) + force_amount_unit = fields.Float( + digits="Ecotax", + help="Force ecotax.\n" "Allow to subtite default Ecotax Classification\n", + ) + amount_total = fields.Float( + digits="Ecotax", + compute="_compute_ecotax", + help="Ecotax Amount total computed form 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_cls = ecotaxline.classification_id + + if ecotax_cls.ecotax_type == "weight_based": + amt = ecotax_cls.ecotax_coef * (ecotaxline.product_id.weight or 0.0) + else: + amt = ecotax_cls.default_fixed_ecotax + # force ecotax amount + if ecotaxline.force_amount_unit: + amt = ecotaxline.force_amount_unit + + 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..aeb4677cf --- /dev/null +++ b/account_ecotax/models/ecotax_line_product.py @@ -0,0 +1,81 @@ +# © 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 mutili ecotax calssification 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\n", + ) + 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..cbd3bc1ed --- /dev/null +++ b/account_ecotax/models/product_product.py @@ -0,0 +1,91 @@ +# 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", + help="Ecotax Amount computed form all ecotax line classification", + store=True, + ) + fixed_ecotax = fields.Float( + compute="_compute_product_ecotax", + help="Fixed ecotax of the Ecotax Classification", + ) + weight_based_ecotax = fields.Float( + compute="_compute_product_ecotax", + 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..13ff903ff --- /dev/null +++ b/account_ecotax/models/product_template.py @@ -0,0 +1,57 @@ +# © 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 form Classification", + store=True, + ) + fixed_ecotax = fields.Float( + compute="_compute_ecotax", + help="Fixed ecotax of the Ecotax Classification", + ) + weight_based_ecotax = fields.Float( + compute="_compute_ecotax", + help="Ecotax value :\n" + "product weight * ecotax coef of " + "Ecotax Classification", + ) + + @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..f55657f6b --- /dev/null +++ b/account_ecotax/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Mourad EL HADJ MIMOUNE diff --git a/account_ecotax/readme/DESCRIPTION.rst b/account_ecotax/readme/DESCRIPTION.rst new file mode 100644 index 000000000..a0a6d4172 --- /dev/null +++ b/account_ecotax/readme/DESCRIPTION.rst @@ -0,0 +1,31 @@ +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 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..b8d93a29d --- /dev/null +++ b/account_ecotax/readme/USAGE.rst @@ -0,0 +1,10 @@ +Add ecotaxe classification via the menu *Accounting > configuration > Taxes > Ecotaxe Classification*. +Ecotaxe classification is either a fixed ecotaxe or weight based ecotaxe. +ecotaxe classification Infos can be used for legal declarations. +For fixed ecotaxe, ecotaxe amount is used as default value. We can for ecotaxe amount on product. + +For weight based ecotaxe, we should define one ecotaxe by coef applied for the weight (depending on product materials). + +Assign one or more ecotaxe classification to a product. + +we can also force amount ecotaxe on account move line by classification. 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 0000000000000000000000000000000000000000..ee185a69f467c138ecfc6c2f234ed911e50860da GIT binary patch literal 3731 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 + + + + +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 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

+

Add ecotaxe classification via the menu Accounting > configuration > Taxes > Ecotaxe Classification. +Ecotaxe classification is either a fixed ecotaxe or weight based ecotaxe. +ecotaxe classification Infos can be used for legal declarations. +For fixed ecotaxe, ecotaxe amount is used as default value. We can for ecotaxe amount on product.

+

For weight based ecotaxe, we should define one ecotaxe by coef applied for the weight (depending on product materials).

+

Assign one or more ecotaxe classification to a product.

+

we can also force amount ecotaxe on account move line by classification.

+
+
+

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.

+

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..c002ab77c --- /dev/null +++ b/account_ecotax/tests/test_ecotax.py @@ -0,0 +1,437 @@ +# 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.tests.common import Form, tagged + +from odoo.addons.account.tests.common import AccountTestInvoicingCommon + + +@tagged("-at_install", "post_install") +class TestInvoiceEcotaxe(AccountTestInvoicingCommon): + @classmethod + def setUpClass(cls, chart_template_ref="l10n_fr.l10n_fr_pcg_chart_template"): + super().setUpClass(chart_template_ref) + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + + # 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": "FAB", + } + ) + # 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": "FAB", + } + ) + + # 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, + }, + ), + ], + } + ) + + # 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""" + return cls.init_invoice( + "out_invoice", + partner=cls.invoice_partner, + products=products, + company=cls.env.user.company_id, + taxes=cls.invoice_tax, + ) + + @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, + } + ) + 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_force_ecotax_on_invoice(self): + """Test force fixed ecotax + + Ecotax classification data for this test: + - fixed type + - default amount: 5.0 + - forced amount: 2 + Product data for this test: + - list price: 100 + - fixed ecotax : 5 + + Expected results (with 1 line and qty = 1): + - invoice ecotax amount: 2.0 + - invoice total amount: 100.0 + - line ecotax unit amount: 2.0 + - line ecotax total amount: 2.0 + """ + product = self._make_product(self.ecotax_fixed) + invoice = self._make_invoice(products=product) + invoice.invoice_line_ids[0].ecotax_line_ids.force_amount_unit = 2 + self._run_checks( + invoice, + {"amount_ecotax": 2.0, "amount_total": 100.0}, + [{"ecotax_amount_unit": 2.0, "subtotal_ecotax": 2.0}], + ) + new_qty = self._set_invoice_lines_random_quantities(invoice)[0] + self._run_checks( + invoice, + {"amount_ecotax": 2.0 * new_qty, "amount_total": 100.0 * new_qty}, + [{"ecotax_amount_unit": 2.0, "subtotal_ecotax": 2.0 * new_qty}], + ) + + def test_06_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, + ) 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..a12faef73 --- /dev/null +++ b/account_ecotax/views/account_ecotax_classification_view.xml @@ -0,0 +1,138 @@ + + + + + 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..a0e889c9b --- /dev/null +++ b/account_ecotax/views/account_move_view.xml @@ -0,0 +1,176 @@ + + + + + l10n_fr_ecotax.move.form + account.move + + + + + + + +