Skip to content

Commit

Permalink
[REF] sale_margin_tax: refactor for correct accounting
Browse files Browse the repository at this point in the history
When accounting for margin tax sales, the purchase price
and margin (that form the base for the tax lines) need to be split.
This is because a different tax grid should be applied on both line.
However, Odoo does not provide a way to split the base as it does to
split the tax. Furthermore, adding a 'margin' line type does not work
because the functions that apply the repartition lines etc are basically
impossible to override (they do not provide any hook,
and the arbitrary compute order would make any tentative very brittle).
To avoid these difficulty, another line is created on a "margin" product
(a service), so that both the purchase and the margin can be accounted
as base lines.

This provide the advantage that no further override of basic accounting
functions are needed, and that the tax rate of the margin tax is always
either the initial rate (say 21) or 0.

The downside is that it makes so statistics harder.
Since the margin line and the product lines are linked by a many2one,
it is still possible to extract the relevant info, just a bit harder.

This refactoring also merges the margin behaviour from invoice and sale
into a mixin to avoid desync of the essentially duplicated code
(if only both line/tax fields had the same name...).
This solved a bug where an invoice would think to be margin-based but
without finding a witness margin tax.

Last, this now handles the case where the margin is negative,
in which case no tax is due.
  • Loading branch information
len-foss committed Apr 29, 2024
1 parent 3f3c9d2 commit e73dacb
Show file tree
Hide file tree
Showing 17 changed files with 389 additions and 61 deletions.
2 changes: 1 addition & 1 deletion sale_margin_tax/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions sale_margin_tax/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"data": [
"views/tax.xml",
"data/account_tax.xml",
"data/product.xml",
"reports/report_invoice_document.xml",
],
}
9 changes: 9 additions & 0 deletions sale_margin_tax/data/account_tax.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@
<odoo noupdate="1">
<record id="tax_group_margin" model="account.tax.group">
<field name="name">[Margin Tax]</field>
</record>
<record id="tax_margin_null" model="account.tax">
<field name="name">Margin 0%</field>
<field name="amount_type">margin</field>
<field name="margin_description_template">Margin Tax</field>
<field name="amount">0</field>
<field name="price_include" eval="True" />
<field name="tax_group_id" ref="tax_group_margin" />
</record>
<record id="tax_margin" model="account.tax">
<field name="name">Margin 21%</field>
Expand All @@ -10,5 +18,6 @@
<field name="amount">21</field>
<field name="price_include" eval="True" />
<field name="tax_group_id" ref="tax_group_margin" />
<field name="margin_base_tax_id" ref="tax_margin_null" />
</record>
</odoo>
7 changes: 7 additions & 0 deletions sale_margin_tax/data/product.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<record id="product_margin" model="product.product">
<field name="name">Margin on secondhand sales</field>
<field name="type">service</field>
</record>
</odoo>
140 changes: 140 additions & 0 deletions sale_margin_tax/i18n/sale_margin_tax.pot
Original file line number Diff line number Diff line change
@@ -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 ""
1 change: 1 addition & 0 deletions sale_margin_tax/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from . import margin_mixin
from . import account_tax
from . import sale_order_line
from . import sale_order
Expand Down
33 changes: 15 additions & 18 deletions sale_margin_tax/models/account_move.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
30 changes: 21 additions & 9 deletions sale_margin_tax/models/account_move_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
14 changes: 14 additions & 0 deletions sale_margin_tax/models/account_tax.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
)
Expand All @@ -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;
Expand Down
68 changes: 68 additions & 0 deletions sale_margin_tax/models/margin_mixin.py
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit e73dacb

Please sign in to comment.