diff --git a/sale_margin_tax/README.rst b/sale_margin_tax/README.rst index 14974dcf..8a6d738c 100644 --- a/sale_margin_tax/README.rst +++ b/sale_margin_tax/README.rst @@ -7,7 +7,7 @@ Sale Margin Tax !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:416d0b79eaaa0c0f1e1a0e5a318854e8c96763da78689885a682a6dab2556725 + !! source digest: sha256:c643555de95864a024f53fe400bc41b671c13907fe80729c92c7365c5de15f28 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png diff --git a/sale_margin_tax/__manifest__.py b/sale_margin_tax/__manifest__.py index 57a51572..59a3a696 100644 --- a/sale_margin_tax/__manifest__.py +++ b/sale_margin_tax/__manifest__.py @@ -14,6 +14,7 @@ "data": [ "views/tax.xml", "data/account_tax.xml", + "data/product.xml", "reports/report_invoice_document.xml", ], } diff --git a/sale_margin_tax/data/account_tax.xml b/sale_margin_tax/data/account_tax.xml index e23a357b..cd88696a 100644 --- a/sale_margin_tax/data/account_tax.xml +++ b/sale_margin_tax/data/account_tax.xml @@ -2,6 +2,14 @@ [Margin Tax] + + + Margin 0% + margin + Margin Tax + 0 + + Margin 21% @@ -10,5 +18,6 @@ 21 + diff --git a/sale_margin_tax/data/product.xml b/sale_margin_tax/data/product.xml new file mode 100644 index 00000000..6200532d --- /dev/null +++ b/sale_margin_tax/data/product.xml @@ -0,0 +1,7 @@ + + + + Margin on secondhand sales + service + + diff --git a/sale_margin_tax/i18n/sale_margin_tax.pot b/sale_margin_tax/i18n/sale_margin_tax.pot new file mode 100644 index 00000000..af40a222 --- /dev/null +++ b/sale_margin_tax/i18n/sale_margin_tax.pot @@ -0,0 +1,140 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * sale_margin_tax +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0+e\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: sale_margin_tax +#: model:ir.model.fields,help:sale_margin_tax.field_account_tax__amount_type +msgid "" +"\n" +" - Group of Taxes: The tax is a set of sub taxes.\n" +" - Fixed: The tax amount stays the same whatever the price.\n" +" - Percentage of Price: The tax amount is a % of the price:\n" +" e.g 100 * (1 + 10%) = 110 (not price included)\n" +" e.g 110 / (1 + 10%) = 100 (price included)\n" +" - Percentage of Price Tax Included: The tax amount is a division of the price:\n" +" e.g 180 / (1 - 10%) = 200 (not price included)\n" +" e.g 200 * (1 - 10%) = 180 (price included)\n" +" " +msgstr "" + +#. module: sale_margin_tax +#: model:ir.model.fields,field_description:sale_margin_tax.field_account_bank_statement_line__has_margin_taxes +#: model:ir.model.fields,field_description:sale_margin_tax.field_account_move__has_margin_taxes +#: model:ir.model.fields,field_description:sale_margin_tax.field_account_move_line__has_margin_taxes +#: model:ir.model.fields,field_description:sale_margin_tax.field_account_payment__has_margin_taxes +#: model:ir.model.fields,field_description:sale_margin_tax.field_sale_order__has_margin_taxes +msgid "Has Margin Taxes" +msgstr "" + +#. module: sale_margin_tax +#: model:ir.model,name:sale_margin_tax.model_account_move +msgid "Journal Entry" +msgstr "" + +#. module: sale_margin_tax +#: model:ir.model,name:sale_margin_tax.model_account_move_line +msgid "Journal Item" +msgstr "" + +#. module: sale_margin_tax +#: model:account.tax,name:sale_margin_tax.tax_margin +msgid "Margin 21%" +msgstr "" + +#. module: sale_margin_tax +#: model:account.tax,margin_mention:sale_margin_tax.tax_margin +msgid "Margin EN mention" +msgstr "" + +#. module: sale_margin_tax +#: model:account.tax,margin_description_template:sale_margin_tax.tax_margin +msgid "Margin Tax" +msgstr "" + +#. module: sale_margin_tax +#: model:account.tax,margin_name_template:sale_margin_tax.tax_margin +msgid "Margin: {}%" +msgstr "" + +#. module: sale_margin_tax +#: model:ir.model.fields,field_description:sale_margin_tax.field_account_tax__margin_mention +msgid "Mention on invoice" +msgstr "" + +#. module: sale_margin_tax +#: model:ir.model,name:sale_margin_tax.model_sale_order +msgid "Sales Order" +msgstr "" + +#. module: sale_margin_tax +#: model:ir.model,name:sale_margin_tax.model_sale_order_line +msgid "Sales Order Line" +msgstr "" + +#. module: sale_margin_tax +#: model:ir.model.fields,field_description:sale_margin_tax.field_account_move_line__price_subtotal_report +msgid "Subtotal (Report)" +msgstr "" + +#. module: sale_margin_tax +#: model:ir.model,name:sale_margin_tax.model_account_tax +msgid "Tax" +msgstr "" + +#. module: sale_margin_tax +#: model:ir.model.fields,field_description:sale_margin_tax.field_account_tax__amount_type +msgid "Tax Computation" +msgstr "" + +#. module: sale_margin_tax +#: model:ir.model.fields.selection,name:sale_margin_tax.selection__account_tax__amount_type__margin +msgid "Tax on margin" +msgstr "" + +#. module: sale_margin_tax +#: model:ir.model.fields,field_description:sale_margin_tax.field_account_tax__margin_description_template +msgid "Template used for the generated tax description" +msgstr "" + +#. module: sale_margin_tax +#: model:ir.model.fields,field_description:sale_margin_tax.field_account_tax__margin_name_template +msgid "Template used for the generated tax names" +msgstr "" + +#. module: sale_margin_tax +#: model:ir.model.fields,help:sale_margin_tax.field_account_tax__margin_mention +msgid "This mention will be automatically added to the invoice." +msgstr "" + +#. module: sale_margin_tax +#: model:ir.model.fields,help:sale_margin_tax.field_account_tax__margin_description_template +msgid "This name can accept the rate as parameter, e.g. use 'Margin: {:.2f}%'" +msgstr "" + +#. module: sale_margin_tax +#: model:ir.model.fields,help:sale_margin_tax.field_account_tax__margin_name_template +msgid "This name must accept the rate as parameter, e.g. use 'Margin: {}%'" +msgstr "" + +#. module: sale_margin_tax +#. odoo-python +#: code:addons/sale_margin_tax/models/account_tax.py:0 +#, python-format +msgid "Untaxed Amount" +msgstr "" + +#. module: sale_margin_tax +#: model:account.tax.group,name:sale_margin_tax.tax_group_margin +msgid "[Margin Tax]" +msgstr "" diff --git a/sale_margin_tax/models/__init__.py b/sale_margin_tax/models/__init__.py index c81d98b2..f340edec 100644 --- a/sale_margin_tax/models/__init__.py +++ b/sale_margin_tax/models/__init__.py @@ -1,3 +1,4 @@ +from . import margin_mixin from . import account_tax from . import sale_order_line from . import sale_order diff --git a/sale_margin_tax/models/account_move.py b/sale_margin_tax/models/account_move.py index bc8247a1..982d1558 100644 --- a/sale_margin_tax/models/account_move.py +++ b/sale_margin_tax/models/account_move.py @@ -2,27 +2,24 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from odoo import api, fields, models +from odoo import fields, models class AccountMove(models.Model): - _inherit = "account.move" - - has_margin_taxes = fields.Boolean(compute="_compute_has_margin_taxes") - - @api.depends("line_ids.tax_ids") - def _compute_has_margin_taxes(self): - for move in self: - move.has_margin_taxes = any(move.mapped("line_ids.has_margin_taxes")) - - def _get_margin_taxes(self): - filter_tax = lambda t: t.amount_type == "margin" # noqa: E731 - return self.line_ids.tax_ids.filtered(filter_tax) - - def _get_margin_mention(self): - margin_tax = self._get_margin_taxes()[0] - margin_tax = margin_tax.with_context(lang=self.partner_id.lang) - return margin_tax.margin_mention + _name = "account.move" + _inherit = ["account.move", "margin.mixin"] + + invoice_report_line_ids = fields.One2many( + "account.move.line", + "move_id", + string="Invoice Report lines", + copy=False, + readonly=True, + domain=[ + ("display_type", "in", ("product", "line_section", "line_note")), + ("is_margin_line", "=", False), + ], + ) def action_post(self): res = super().action_post() diff --git a/sale_margin_tax/models/account_move_line.py b/sale_margin_tax/models/account_move_line.py index 8383e435..6a2d1940 100644 --- a/sale_margin_tax/models/account_move_line.py +++ b/sale_margin_tax/models/account_move_line.py @@ -6,23 +6,35 @@ class AccountMoveLine(models.Model): - _inherit = "account.move.line" + _name = "account.move.line" + _inherit = ["account.move.line", "margin.line.mixin"] - has_margin_taxes = fields.Boolean(compute="_compute_has_margin_taxes") price_subtotal_report = fields.Monetary( string="Subtotal (Report)", compute="_compute_price_subtotal_report", currency_field="currency_id", ) - - @api.depends("tax_ids") - def _compute_has_margin_taxes(self): - mg = self.env.ref("sale_margin_tax.tax_group_margin") - for line in self: - line.has_margin_taxes = mg in line.mapped("tax_ids.tax_group_id") + price_unit_report = fields.Monetary( + string="Price Unit (Report)", + compute="_compute_price_subtotal_report", + currency_field="currency_id", + ) + is_margin_line = fields.Boolean( + help="True if this line is a margin line.", + default=False, + ) + margin_line_id = fields.Many2one( + "account.move.line", + help="The margin line for this line.", + ) @api.depends("price_subtotal", "price_total", "has_margin_taxes") def _compute_price_subtotal_report(self): for line in self: - price = line.price_total if line.has_margin_taxes else line.price_subtotal + price = line.price_subtotal + price_unit = line.price_unit + if line.has_margin_taxes: + price = line.price_total + line.margin_line_id.price_total + price_unit = price_unit + line.margin_line_id.price_unit line.price_subtotal_report = price + line.price_unit_report = price_unit diff --git a/sale_margin_tax/models/account_tax.py b/sale_margin_tax/models/account_tax.py index e6c8080e..de023e7a 100644 --- a/sale_margin_tax/models/account_tax.py +++ b/sale_margin_tax/models/account_tax.py @@ -33,6 +33,10 @@ class AccountTax(models.Model): _inherit = "account.tax" + is_margin_tax = fields.Boolean( + compute="_compute_is_margin_tax", + help="True if this tax is a margin tax.", + ) amount_type = fields.Selection( selection_add=[("margin", "Tax on margin")], ondelete={"margin": "cascade"} ) @@ -55,6 +59,16 @@ class AccountTax(models.Model): default="Margin: {:.2f}%", help="This name can accept the rate as parameter, e.g. use 'Margin: {:.2f}%'", ) + margin_base_tax_id = fields.Many2one( + "account.tax", + string="Base Tax", + help="The tax to be applied on the base amount of the margin tax.", + ) + + def _compute_is_margin_tax(self): + margin_group = self.env.ref("sale_margin_tax.tax_group_margin") + for tax in self: + tax.is_margin_tax = tax.tax_group_id == margin_group def _get_or_create_margin_tax(self, base_amount, margin): """On non margin tax, simply return the tax; diff --git a/sale_margin_tax/models/margin_mixin.py b/sale_margin_tax/models/margin_mixin.py new file mode 100644 index 00000000..08733c6b --- /dev/null +++ b/sale_margin_tax/models/margin_mixin.py @@ -0,0 +1,68 @@ +# Copyright 2023 len-foss/Financial Way +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + + +from odoo import api, fields, models + + +class MarginLineMixin(models.AbstractModel): + _name = "margin.line.mixin" + _description = "Margin Line Mixin" + + has_margin_taxes = fields.Boolean(compute="_compute_has_margin_taxes") + + @api.model + def _get_tax_field(self): + return "tax_id" if self._name == "sale.order.line" else "tax_ids" + + @api.model + def _get_margin_depends(self): + tax_field = self._get_tax_field() + return [tax_field + ".is_margin_tax"] + + def _get_line_taxes(self): + tax_field = self._get_tax_field() + return self[tax_field] + + def _get_margin_taxes(self): + taxes = self._get_line_taxes() + return taxes.filtered("is_margin_tax") + + @api.depends(lambda self: self._get_margin_depends()) + def _compute_has_margin_taxes(self): + for line in self: + line.has_margin_taxes = line._get_margin_taxes() + + +class MarginMixin(models.AbstractModel): + _name = "margin.mixin" + _description = "Margin Mixin" + + has_margin_taxes = fields.Boolean(compute="_compute_has_margin_taxes") + + @api.model + def _get_margin_depends(self): + line_field = self._get_lines_field() + return [line_field + ".has_margin_taxes"] + + def _get_lines_field(self): + return "order_line" if self._name == "sale.order" else "line_ids" + + def _get_lines(self): + line_field = self._get_lines_field() + return self[line_field] + + @api.depends(lambda self: self._get_margin_depends()) + def _compute_has_margin_taxes(self): + for record in self: + lines = record._get_lines() + record.has_margin_taxes = any(lines.mapped("has_margin_taxes")) + + def _get_margin_taxes(self): + lines = self._get_lines() + return lines._get_margin_taxes() + + def _get_margin_mention(self): + margin_tax = self._get_margin_taxes()[0] + margin_tax = margin_tax.with_context(lang=self.partner_id.lang) + return margin_tax.margin_mention diff --git a/sale_margin_tax/models/sale_order.py b/sale_margin_tax/models/sale_order.py index 3a9f1f10..78ba2279 100644 --- a/sale_margin_tax/models/sale_order.py +++ b/sale_margin_tax/models/sale_order.py @@ -1,27 +1,12 @@ # Copyright 2023 len-foss/Financial Way # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from odoo import api, fields, models +from odoo import models class SaleOrder(models.Model): - _inherit = "sale.order" - - has_margin_taxes = fields.Boolean(compute="_compute_has_margin_taxes") - - @api.depends("order_line.tax_id") - def _compute_has_margin_taxes(self): - for order in self: - order.has_margin_taxes = order._get_margin_taxes() - - def _get_margin_taxes(self): - filter_tax = lambda t: t.amount_type == "margin" # noqa: E731 - return self.order_line.tax_id.filtered(filter_tax) - - def _get_margin_mention(self): - margin_tax = self._get_margin_taxes()[0] - margin_tax = margin_tax.with_context(lang=self.partner_id.lang) - return margin_tax.margin_mention + _name = "sale.order" + _inherit = ["sale.order", "margin.mixin"] def action_confirm(self): res = super().action_confirm() @@ -32,3 +17,15 @@ def action_confirm(self): if msg not in note: order.note = note + "\n" + msg return res + + def _create_invoices(self, grouped=False, final=False): + res = super()._create_invoices(grouped=grouped, final=final) + for invoice in res: + if self.has_margin_taxes: + for line in self.order_line.filtered("has_margin_taxes"): + invoice_line = line.invoice_lines # will crash in many cases... + vals_line = line._prepare_invoice_margin_base_line() + vals_line["move_id"] = invoice.id + vals_line["margin_line_id"] = invoice_line.id + self.env["account.move.line"].create(vals_line) + return res diff --git a/sale_margin_tax/models/sale_order_line.py b/sale_margin_tax/models/sale_order_line.py index b6d92526..1d95db9a 100644 --- a/sale_margin_tax/models/sale_order_line.py +++ b/sale_margin_tax/models/sale_order_line.py @@ -7,7 +7,8 @@ class SaleOrderLine(models.Model): - _inherit = "sale.order.line" + _name = "sale.order.line" + _inherit = ["sale.order.line", "margin.line.mixin"] def _get_taxes(self): """Override to get the purchase price from lot, purchase order, etc. @@ -21,14 +22,38 @@ def _get_purchase_price(self): """ return self.product_id.standard_price * self.product_uom_qty + def _get_margin(self): + purchase_price = self._get_purchase_price() + return max(self.price_subtotal - purchase_price, 0) + + def _get_margin_base_price(self): + base_price = self.price_subtotal + margin = self._get_margin() + return base_price - margin + def _prepare_invoice_line(self, **optional_values): vals = super()._prepare_invoice_line(**optional_values) taxes = self._get_taxes() if "margin" in taxes.mapped("amount_type"): - purchase_price = self._get_purchase_price() - taxes = taxes._apply_margin_taxes(self.price_subtotal, purchase_price) - if taxes: - vals["tax_ids"] = [(6, 0, taxes.ids)] - else: - vals.pop("tax_ids", False) + vals["is_margin_line"] = True + qty = self.product_uom_qty + product = self.env.ref("sale_margin_tax.product_margin") + vals["product_id"] = product.id + margin = self._get_margin() + taxes = taxes._apply_margin_taxes(margin, 0) + vals["price_unit"] = margin / qty + vals["tax_ids"] = [(6, 0, taxes.ids)] if taxes else [(5, 0, 0)] + return vals + + def _prepare_invoice_margin_base_line(self, **optional_values): + vals = super()._prepare_invoice_line(**optional_values) + taxes = self._get_taxes() + tax_null = taxes.margin_base_tax_id + qty = self.product_uom_qty + base_values = { + "price_unit": self._get_margin_base_price() / qty, + "quantity": qty, + "tax_ids": [(6, 0, tax_null.ids)], + } + vals.update(base_values) return vals diff --git a/sale_margin_tax/reports/report_invoice_document.xml b/sale_margin_tax/reports/report_invoice_document.xml index 775ea23e..2d11703a 100644 --- a/sale_margin_tax/reports/report_invoice_document.xml +++ b/sale_margin_tax/reports/report_invoice_document.xml @@ -5,8 +5,14 @@ diff --git a/sale_margin_tax/static/description/index.html b/sale_margin_tax/static/description/index.html index 05c84f5a..f769da07 100644 --- a/sale_margin_tax/static/description/index.html +++ b/sale_margin_tax/static/description/index.html @@ -367,7 +367,7 @@

Sale Margin Tax

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:416d0b79eaaa0c0f1e1a0e5a318854e8c96763da78689885a682a6dab2556725 +!! source digest: sha256:c643555de95864a024f53fe400bc41b671c13907fe80729c92c7365c5de15f28 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Beta License: AGPL-3 OCA/l10n-belgium Translate me on Weblate Try me on Runboat

This module takes a simple approach to the Belgian margin tax.

diff --git a/sale_margin_tax/tests/common.py b/sale_margin_tax/tests/common.py index 53605da3..2f7c4619 100644 --- a/sale_margin_tax/tests/common.py +++ b/sale_margin_tax/tests/common.py @@ -16,7 +16,17 @@ def setUpClass(cls): vals_product_2 = {"name": "P2", "standard_price": 5} cls.product_2 = cls.env["product.product"].create(vals_product_2) - vals_tax = {"name": "Margin Tax", "amount": 21.0, "amount_type": "margin"} + margin_group = cls.env.ref("sale_margin_tax.tax_group_margin") + + vals_tax_null = {"name": "M0", "amount": 0, "amount_type": "margin"} + cls.tax_null = cls.env["account.tax"].create(vals_tax_null) + vals_tax = { + "name": "Margin Tax", + "amount": 21.0, + "amount_type": "margin", + "margin_base_tax_id": cls.tax_null.id, + "tax_group_id": margin_group.id, + } cls.tax_margin = cls.env["account.tax"].create(vals_tax) vals_tax_other = {"name": "Other Tax", "amount": 21.0, "amount_type": "percent"} @@ -26,13 +36,20 @@ def setUpClass(cls): cls.tag_vat = cls.env["account.account.tag"].create(vals_tag_vat) vals_tag_base = {"name": "Base", "applicability": "taxes"} cls.tag_base = cls.env["account.account.tag"].create(vals_tag_base) - cls.tags = cls.tag_base + cls.tag_vat + vals_tag_margin = {"name": "Margin", "applicability": "taxes"} + cls.tag_margin = cls.env["account.account.tag"].create(vals_tag_margin) + cls.tags = cls.tag_base + cls.tag_vat + cls.tag_margin lines = cls.tax_margin.invoice_repartition_line_ids - line_base = lines.filtered(lambda l: l.repartition_type == "base") - line_margin = lines.filtered(lambda l: l.repartition_type == "tax") + line_margin = lines.filtered(lambda l: l.repartition_type == "base") + line_tax = lines.filtered(lambda l: l.repartition_type == "tax") + line_margin.tag_ids = [(6, 0, cls.tag_margin.ids)] + line_tax.tag_ids = [(6, 0, cls.tag_vat.ids)] + + # the repartition line for the base is on the null tax + rls = cls.tax_null.invoice_repartition_line_ids + line_base = rls.filtered(lambda l: l.repartition_type == "base") line_base.tag_ids = [(6, 0, cls.tag_base.ids)] - line_margin.tag_ids = [(6, 0, cls.tag_vat.ids)] vals_sale = { "partner_id": cls.partner.id, @@ -71,4 +88,3 @@ def setUpClass(cls): langs = cls.env["res.lang"].with_context(active_test=False) cls.lang = langs.search([("code", "=", "fr_BE")]) cls.lang.active = True - # cls.env["ir.translation"].load_module_terms(["base"], [cls.lang.code]) diff --git a/sale_margin_tax/tests/test_tax.py b/sale_margin_tax/tests/test_tax.py index 348ba1e2..91e6b53e 100644 --- a/sale_margin_tax/tests/test_tax.py +++ b/sale_margin_tax/tests/test_tax.py @@ -55,6 +55,33 @@ def test_mixed_taxes(self): self.assertEqual(invoice.amount_total, 136) self.assertTrue("marg" in invoice.narration) + def test_negative_margin(self): + # given: + self.sale.order_line[1].unlink() + # margin is -2 since we buy it for 10 + self.sale.order_line[0].price_unit = 8 + + # when + self.sale.action_confirm() + + # then: tax is properly applied for the first line + self.assertEqual(self.sale.amount_tax, 0) + # total: 4 * 8 = 32 + self.assertEqual(self.sale.amount_total, 4 * 8) + # margin is negative but the mention should be there anyway + self.assertTrue("marg" in self.sale.note) + + # when + invoice = self.sale._create_invoices() + + # then: invoice values are coherent with it + self.assertAlmostEqual(invoice.amount_tax, 0) + self.assertEqual(invoice.amount_total, 32) + self.assertTrue("marg" in invoice.narration) + # the margin line is 0 + margin_line = invoice.invoice_line_ids.filtered("is_margin_line") + self.assertEqual(margin_line.balance, 0) + def test_no_margin_tax(self): """Sell one product with normal tax; nothing fancy should happen.""" # given diff --git a/sale_margin_tax/views/tax.xml b/sale_margin_tax/views/tax.xml index 4b63eaa8..e76d9eba 100644 --- a/sale_margin_tax/views/tax.xml +++ b/sale_margin_tax/views/tax.xml @@ -30,6 +30,10 @@ name="margin_description_template" attrs="{'invisible':[('amount_type','!=', 'margin')]}" /> +