[FIX] account, point_of_sale: Fix complex taxes computation

The previous _compute_all wasn't able to get the right results in case of complex
taxes computation.

See tests for examples.

task: 39945
This commit is contained in:
Laurent Smet
2019-05-09 11:56:39 +00:00
committed by Olivier Colson
parent 1ab4b61059
commit 4523a11bed
5 changed files with 630 additions and 165 deletions
+152 -58
View File
@@ -901,7 +901,17 @@ class AccountTax(models.Model):
type_tax_use = fields.Selection([('sale', 'Sales'), ('purchase', 'Purchases'), ('none', 'None'), ('adjustment', 'Adjustment')], string='Tax Scope', required=True, default="sale",
help="Determines where the tax is selectable. Note : 'None' means a tax can't be used by itself, however it can still be used in a group. 'adjustment' is used to perform tax adjustment.")
amount_type = fields.Selection(default='percent', string="Tax Computation", required=True, oldname='type',
selection=[('group', 'Group of Taxes'), ('fixed', 'Fixed'), ('percent', 'Percentage of Price'), ('division', 'Percentage of Price Tax Included')])
selection=[('group', 'Group of Taxes'), ('fixed', 'Fixed'), ('percent', 'Percentage of Price'), ('division', 'Percentage of Price Tax Included')],
help="""
- Group of Taxes: The tax is a set of sub taxes.
- Fixed: The tax amount stays the same whatever the price.
- Percentage of Price: The tax amount is a % of the price:
e.g 100 * 10% = 110 (not price included)
e.g 110 / (1 + 10%) = 100 (price included)
- Percentage of Price Tax Included: The tax amount is a division of the price:
e.g 180 / (1 - 10%) = 200 (not price included)
e.g 200 * (1 - 10%) = 180 (price included)
""")
active = fields.Boolean(default=True, help="Set active to false to hide the tax without removing it.")
company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.user.company_id)
children_tax_ids = fields.Many2many('account.tax', 'account_tax_filiation_rel', 'parent_tax', 'child_tax', string='Children Taxes')
@@ -1027,6 +1037,7 @@ class AccountTax(models.Model):
price_unit * quantity eventually affected by previous taxes (if tax is include_base_amount XOR price_include)
"""
self.ensure_one()
if self.amount_type == 'fixed':
# Use copysign to take into account the sign of the base amount which includes the sign
# of the quantity and the sign of the price_unit
@@ -1041,14 +1052,20 @@ class AccountTax(models.Model):
else:
return quantity * self.amount
price_include = self.price_include or self._context.get('force_price_include')
price_include = self._context['force_price_include'] if 'force_price_include' in self._context else self.price_include
if (self.amount_type == 'percent' and not price_include) or (self.amount_type == 'division' and price_include):
# base * (1 + tax_amount) = new_base
if self.amount_type == 'percent' and not price_include:
return base_amount * self.amount / 100
# <=> new_base = base / (1 + tax_amount)
if self.amount_type == 'percent' and price_include:
return base_amount - (base_amount / (1 + self.amount / 100))
# base / (1 - tax_amount) = new_base
if self.amount_type == 'division' and not price_include:
return base_amount / (1 - self.amount / 100) - base_amount
# <=> new_base * (1 - tax_amount) = base
if self.amount_type == 'division' and price_include:
return base_amount - (base_amount * (self.amount / 100))
@api.multi
def json_friendly_compute_all(self, price_unit, currency_id=None, quantity=1.0, product_id=None, partner_id=None):
@@ -1080,15 +1097,44 @@ class AccountTax(models.Model):
'account_id': int,
'refund_account_id': int,
'analytic': boolean,
}]
}],
} """
if len(self) == 0:
company_id = self.env.user.company_id
if not self:
company = self.env.user.company_id
else:
company_id = self[0].company_id
company = self[0].company_id
# 1) Flatten the taxes.
def collect_taxes(self):
# Collect all the taxes recursively and ordered by sequence.
# Eg. considering letters as taxes and alphabetic order as sequence :
# [G, B([A, D, F]), E, C] will be computed as [A, D, F, C, E, G]
all_taxes = []
for tax in self.sorted(key=lambda r: r.sequence):
if tax.amount_type == 'group':
all_taxes += collect_taxes(tax.children_tax_ids)
else:
all_taxes.append(tax)
return all_taxes
taxes = collect_taxes(self)
# 2) Avoid mixing taxes having price_include=False && include_base_amount=True
# with taxes having price_include=True. This use case is not supported as the
# computation of the total_excluded would be impossible.
base_excluded_flag = False # price_include=False && include_base_amount=True
included_flag = False # price_include=True
for tax in taxes:
if tax.price_include:
included_flag = True
elif tax.include_base_amount:
base_excluded_flag = True
if base_excluded_flag and included_flag:
raise UserError(_('Unable to mix any taxes being price included with taxes affecting the base amount but not included in price.'))
# 3) Deal with the rounding methods
if not currency:
currency = company_id.currency_id
taxes = []
currency = company.currency_id
# By default, for each tax, tax amount will first be computed
# and rounded at the 'Account' decimal precision for each
# PO/SO/invoice line and then these rounded amounts will be
@@ -1104,7 +1150,7 @@ class AccountTax(models.Model):
# amounts. For example, in SO/PO line, we don't want to round the price unit at the
# precision of the currency.
# The context key 'round' allows to force the standard behavior.
round_tax = False if company_id.tax_calculation_rounding_method == 'round_globally' else True
round_tax = False if company.tax_calculation_rounding_method == 'round_globally' else True
round_total = True
if 'round' in self.env.context:
round_tax = bool(self.env.context['round'])
@@ -1113,59 +1159,102 @@ class AccountTax(models.Model):
if not round_tax:
prec += 5
base_values = self.env.context.get('base_values')
if not base_values:
total_excluded = total_included = total_void = base = round(price_unit * quantity, prec)
else:
total_excluded, total_included, total_void, base = base_values
# 4) Iterate the taxes in the reversed sequence order to retrieve the initial base of the computation.
# tax | base | amount |
# /\ ----------------------------
# || tax_1 | XXXX | | <- we are looking for that, it's the total_excluded
# || tax_2 | .. | |
# || tax_3 | .. | |
# || ... | .. | .. |
# ----------------------------
def recompute_base(base_amount, fixed_amount, percent_amount, division_amount):
# Recompute the new base amount based on included fixed/percent amounts and the current base amount.
# Example:
# tax | amount | type | price_include |
# -----------------------------------------------
# tax_1 | 10% | percent | t
# tax_2 | 15 | fix | t
# tax_3 | 20% | percent | t
# tax_4 | 10% | division | t
# -----------------------------------------------
# Sorting key is mandatory in this case. When no key is provided, sorted() will perform a
# search. However, the search method is overridden in account.tax in order to add a domain
# depending on the context. This domain might filter out some taxes from self, e.g. in the
# case of group taxes.
for tax in self.sorted(key=lambda r: r.sequence):
# Allow forcing price_include/include_base_amount through the context for the reconciliation widget.
# See task 24014.
price_include = self._context.get('force_price_include', tax.price_include)
# if base_amount = 145, the new base is computed as:
# (145 - 15) / (1.0 + 30%) * 90% = 130 / 1.3 * 90% = 90
return (base_amount - fixed_amount) / (1.0 + percent_amount / 100.0) * (100 - division_amount) / 100
if tax.amount_type == 'group':
children = tax.children_tax_ids.with_context(base_values=(total_excluded, total_included, total_void, base))
ret = children.compute_all(price_unit, currency, quantity, product, partner)
total_excluded = ret['total_excluded']
base = ret['base'] if tax.include_base_amount else base
total_included = ret['total_included']
total_void = ret['total_void']
tax_amount = total_included - total_excluded
taxes += ret['taxes']
continue
base = round(price_unit * quantity, prec)
tax_amount = tax._compute_amount(base, price_unit, quantity, product, partner)
# For the computation of move lines, we could have a negative base value.
# In this case, compute all with positive values and negate them at the end.
sign = 1
if base < 0:
base = -base
sign = -1
# Store the totals to reach when using price_include taxes (only the last price included in row)
total_included_checkpoints = {}
i = len(taxes) - 1
store_included_tax_total = True
# Keep track of the accumulated included fixed/percent amount.
incl_fixed_amount = incl_percent_amount = incl_division_amount = 0
# Store the tax amounts we compute while searching for the total_excluded
cached_tax_amounts = {}
for tax in reversed(taxes):
if tax.include_base_amount:
base = recompute_base(base, incl_fixed_amount, incl_percent_amount, incl_division_amount)
incl_fixed_amount = incl_percent_amount = incl_division_amount = 0
store_included_tax_total = True
if tax.price_include:
if tax.amount_type == 'percent':
incl_percent_amount += tax.amount
elif tax.amount_type == 'division':
incl_division_amount += tax.amount
elif tax.amount_type == 'fixed':
incl_fixed_amount += quantity * tax.amount
else:
# tax.amount_type == other (python)
tax_amount = tax._compute_amount(base, price_unit, quantity, product, partner)
incl_fixed_amount += tax_amount
# Avoid unecessary re-computation
cached_tax_amounts[i] = tax_amount
if store_included_tax_total:
total_included_checkpoints[i] = base
store_included_tax_total = False
i -= 1
total_excluded = recompute_base(base, incl_fixed_amount, incl_percent_amount, incl_division_amount)
# 5) Iterate the taxes in the sequence order to compute missing tax amounts.
# Start the computation of accumulated amounts at the total_excluded value.
base = total_included = total_excluded
taxes_vals = []
i = 0
cumulated_tax_included_amount = 0
for tax in taxes:
#compute the tax_amount
if tax.price_include and total_included_checkpoints.get(i):
# We know the total to reach for that tax, so we make a substraction to avoid any rounding issues
tax_amount = total_included_checkpoints[i] - (base + cumulated_tax_included_amount)
cumulated_tax_included_amount = 0
else:
tax_amount = tax.with_context(force_price_include=False)._compute_amount(
base, price_unit, quantity, product, partner)
# Round the tax_amount
if not round_tax:
tax_amount = round(tax_amount, prec)
else:
tax_amount = currency.round(tax_amount)
if price_include:
total_excluded -= tax_amount
base -= tax_amount
else:
total_included += tax_amount
# The total_void amount is computed as the sum of total_excluded
# with all tax_amount, where tax has an account set
if not tax.account_id:
total_void += tax_amount
if tax.price_include and not total_included_checkpoints.get(i):
cumulated_tax_included_amount += tax_amount
# Keep base amount used for the current tax
tax_base = base
if tax.include_base_amount:
base += tax_amount
taxes.append({
taxes_vals.append({
'id': tax.id,
'name': tax.with_context(**{'lang': partner.lang} if partner else {}).name,
'amount': tax_amount,
'base': tax_base,
'name': partner and tax.with_context(lang=partner.lang).name or tax.name,
'amount': sign * tax_amount,
'base': sign * round(base, prec),
'sequence': tax.sequence,
'account_id': tax.account_id.id,
'refund_account_id': tax.refund_account_id.id,
@@ -1174,12 +1263,17 @@ class AccountTax(models.Model):
'tax_exigibility': tax.tax_exigibility,
})
# Affect subsequent taxes
if tax.include_base_amount:
base += tax_amount
total_included += tax_amount
i += 1
return {
'taxes': sorted(taxes, key=lambda k: k['sequence']),
'total_excluded': currency.round(total_excluded) if round_total else total_excluded,
'total_included': currency.round(total_included) if round_total else total_included,
'total_void': currency.round(total_void) if round_total else total_void,
'base': base,
'taxes': taxes_vals,
'total_excluded': sign * (currency.round(total_excluded) if round_total else total_excluded),
'total_included': sign * (currency.round(total_included) if round_total else total_included),
}
@api.model
+260 -45
View File
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from odoo.addons.account.tests.account_test_users import AccountTestUsers
import time
from odoo.tests import tagged
@@ -27,6 +27,12 @@ class TestTax(AccountTestUsers):
'amount': 10,
'sequence': 3,
})
self.percent_tax_bis = self.tax_model.create({
'name': "Percent tax bis",
'amount_type': 'percent',
'amount': 10,
'sequence': 4,
})
self.division_tax = self.tax_model.create({
'name': "Division tax",
'amount_type': 'division',
@@ -82,47 +88,118 @@ class TestTax(AccountTestUsers):
self.bank_account = self.bank_journal.default_debit_account_id
self.expense_account = self.env['account.account'].search([('user_type_id.type', '=', 'payable')], limit=1) #Should be done by onchange later
def _check_compute_all_results(self, total_included, total_excluded, taxes, res):
self.assertAlmostEqual(res['total_included'], total_included)
self.assertAlmostEqual(res['total_excluded'], total_excluded)
for i in range(0, len(taxes)):
self.assertAlmostEqual(res['taxes'][i]['base'], taxes[i][0])
self.assertAlmostEqual(res['taxes'][i]['amount'], taxes[i][1])
def test_tax_group_of_group_tax(self):
self.fixed_tax.include_base_amount = True
self.group_tax.include_base_amount = True
self.group_of_group_tax.include_base_amount = True
res = self.group_of_group_tax.compute_all(200.0)
self.assertEquals(res['total_excluded'], 200.0)
# After calculation of first group
# base = 210
# total_included = 231
# Base of the first grouped is passed
# Base after the second group (220) is dropped.
# Base of the group of groups is passed out,
# so we obtain base as after first group
self.assertEquals(res['base'], 210.0)
self.assertEquals(res['total_included'], 263.0)
self._check_compute_all_results(
263, # 'total_included'
200, # 'total_excluded'
[
# base , amount | seq | amount | incl | incl_base
# ---------------------------------------------------
(200.0, 10.0), # | 1 | 10 | | t
(210.0, 21.0), # | 3 | 10% | |
(210.0, 10.0), # | 1 | 10 | | t
(220.0, 22.0), # | 3 | 10% | |
# ---------------------------------------------------
],
res
)
def test_tax_group(self):
res = self.group_tax.compute_all(200.0)
self.assertEquals(res['total_excluded'], 200.0)
self.assertEquals(res['total_included'], 230.0)
self.assertEquals(len(res['taxes']), 2)
self.assertEquals(res['taxes'][0]['amount'], 10.0)
self.assertEquals(res['taxes'][1]['amount'], 20.0)
self._check_compute_all_results(
230, # 'total_included'
200, # 'total_excluded'
[
# base , amount | seq | amount | incl | incl_base
# ---------------------------------------------------
(200.0, 10.0), # | 1 | 10 | |
(200.0, 20.0), # | 3 | 10% | |
# ---------------------------------------------------
],
res
)
def test_tax_percent_division(self):
self.division_tax.price_include = True
self.division_tax.include_base_amount = True
res_division = self.division_tax.compute_all(200.0)
self._check_compute_all_results(
200, # 'total_included'
180, # 'total_excluded'
[
# base , amount | seq | amount | incl | incl_base
# ---------------------------------------------------
(180.0, 20.0), # | 4 | 10/ | t | t
# ---------------------------------------------------
],
res_division
)
self.percent_tax.price_include = False
self.percent_tax.include_base_amount = False
res_division = self.division_tax.compute_all(200.0)
res_percent = self.percent_tax.compute_all(200.0)
self.assertEquals(res_division['taxes'][0]['amount'], 20.0)
self.assertEquals(res_percent['taxes'][0]['amount'], 20.0)
res_percent = self.percent_tax.compute_all(100.0)
self._check_compute_all_results(
110, # 'total_included'
100, # 'total_excluded'
[
# base , amount | seq | amount | incl | incl_base
# ---------------------------------------------------
(100.0, 10.0), # | 3 | 10% | |
# ---------------------------------------------------
],
res_percent
)
self.division_tax.price_include = False
self.division_tax.include_base_amount = False
res_division = self.division_tax.compute_all(180.0)
self._check_compute_all_results(
200, # 'total_included'
180, # 'total_excluded'
[
# base, amount | seq | amount | incl | incl_base
# ---------------------------------------------------
(180.0, 20.0), # | 4 | 10/ | |
# ---------------------------------------------------
],
res_division
)
self.percent_tax.price_include = True
self.percent_tax.include_base_amount = True
res_division = self.division_tax.compute_all(200.0)
res_percent = self.percent_tax.compute_all(200.0)
self.assertEquals(res_division['taxes'][0]['amount'], 22.22)
self.assertEquals(res_percent['taxes'][0]['amount'], 18.18)
res_percent = self.percent_tax.compute_all(110.0)
self._check_compute_all_results(
110, # 'total_included'
100, # 'total_excluded'
[
# base, amount | seq | amount | incl | incl_base
# ---------------------------------------------------
(100.0, 10.0), # | 3 | 10% | t | t
# ---------------------------------------------------
],
res_percent
)
self.percent_tax_bis.price_include = True
self.percent_tax_bis.include_base_amount = True
self.percent_tax_bis.amount = 21
res_percent = self.percent_tax_bis.compute_all(7.0)
self._check_compute_all_results(
7.0, # 'total_included'
5.79, # 'total_excluded'
[
# base , amount | seq | amount | incl | incl_base
# ---------------------------------------------------
(5.79, 1.21), # | 3 | 21% | t | t
# ---------------------------------------------------
],
res_percent
)
def test_tax_sequence_normalized_set(self):
self.division_tax.sequence = 1
@@ -130,28 +207,166 @@ class TestTax(AccountTestUsers):
self.percent_tax.sequence = 3
taxes_set = (self.group_tax | self.division_tax)
res = taxes_set.compute_all(200.0)
self.assertEquals(res['taxes'][0]['amount'], 22.22)
self.assertEquals(res['taxes'][1]['amount'], 10.0)
self.assertEquals(res['taxes'][2]['amount'], 20.0)
self._check_compute_all_results(
252.22, # 'total_included'
200, # 'total_excluded'
[
# base , amount | seq | amount | incl | incl_base
# ---------------------------------------------------
(200.0, 22.22), # | 1 | 10/ | |
(200.0, 10.0), # | 2 | 10 | |
(200.0, 20.0), # | 3 | 10% | |
# ---------------------------------------------------
],
res
)
def test_tax_include_base_amount(self):
def test_fixed_tax_include_base_amount(self):
self.fixed_tax.include_base_amount = True
res = self.group_tax.compute_all(200.0)
self.assertEquals(res['total_included'], 231.0)
self._check_compute_all_results(
231, # 'total_included'
200, # 'total_excluded'
[
# base , amount | seq | amount | incl | incl_base
# ---------------------------------------------------
(200.0, 10.0), # | 1 | 10 | | t
(210.0, 21.0), # | 3 | 10% | |
# ---------------------------------------------------
],
res
)
def test_tax_currency(self):
self.division_tax.amount = 15.0
res = self.division_tax.compute_all(200.0, currency=self.env.ref('base.VEF'))
self.assertAlmostEqual(res['total_included'], 235.2941)
self.fixed_tax.price_include = True
self.fixed_tax.include_base_amount = False
res = self.fixed_tax.compute_all(100.0, quantity=2.0)
self._check_compute_all_results(
200, # 'total_included'
180, # 'total_excluded'
[
# base , amount | seq | amount | incl | incl_base
# ---------------------------------------------------
(180.0, 20.0), # | 1 | 20 | | t
# ---------------------------------------------------
],
res
)
def test_tax_with_no_account(self):
self.tax_with_no_account.amount = 10.0
res = self.tax_with_no_account.compute_all(200.0)
self.assertEquals(res['total_included'], 210)
self.assertEquals(res['total_void'], 210)
def test_percent_tax_include_base_amount(self):
self.percent_tax.price_include = True
self.percent_tax.amount = 21.0
res = self.percent_tax.compute_all(7.0)
self._check_compute_all_results(
7.0, # 'total_included'
5.79, # 'total_excluded'
[
# base , amount | seq | amount | incl | incl_base
# ---------------------------------------------------
(5.79, 1.21), # | 3 | 21% | t |
# ---------------------------------------------------
],
res
)
def test_tax_with_account(self):
self.tax_with_account.amount = 10.0
res = self.tax_with_account.compute_all(200.0)
self.assertEquals(res['total_included'], 210)
self.assertEquals(res['total_void'], 200)
self.percent_tax.price_include = True
self.percent_tax.amount = 20.0
res = self.percent_tax.compute_all(399.99)
self._check_compute_all_results(
399.99, # 'total_included'
333.33, # 'total_excluded'
[
# base , amount | seq | amount | incl | incl_base
# ---------------------------------------------------
(333.33, 66.66), # | 3 | 20% | t |
# ---------------------------------------------------
],
res
)
def test_tax_decimals(self):
"""Test the rounding of taxes up to 6 decimals (maximum decimals places allowed for currencies)"""
self.env.user.company_id.currency_id.rounding = 0.000001
self.percent_tax.price_include = True
self.percent_tax.amount = 21.0
res = self.percent_tax.compute_all(7.0)
self._check_compute_all_results(
7.0, # 'total_included'
5.785124, # 'total_excluded'
[
# base , amount | seq | amount | incl | incl_base
# --------------------------------------------------------
(5.785124, 1.214876), # | 3 | 21% | t |
# --------------------------------------------------------
],
res
)
self.percent_tax.price_include = True
self.percent_tax.amount = 20.0
res = self.percent_tax.compute_all(399.999999)
self._check_compute_all_results(
399.999999, # 'total_included'
333.333333, # 'total_excluded'
[
# base , amount | seq | amount | incl | incl_base
# -----------------------------------------------------------
(333.333333, 66.666666), # | 3 | 20% | t |
# -----------------------------------------------------------
],
res
)
def test_advanced_taxes_computation_0(self):
'''Test more advanced taxes computation (see issue 34471).'''
tax_1 = self.env['account.tax'].create({
'name': 'test_advanced_taxes_computation_0_1',
'amount_type': 'percent',
'amount': 10,
'price_include': True,
'include_base_amount': True,
'sequence': 1,
})
tax_2 = self.env['account.tax'].create({
'name': 'test_advanced_taxes_computation_0_2',
'amount_type': 'percent',
'amount': 10,
'sequence': 2,
})
tax_3 = self.env['account.tax'].create({
'name': 'test_advanced_taxes_computation_0_3',
'amount_type': 'percent',
'amount': 10,
'price_include': True,
'sequence': 3,
})
tax_4 = self.env['account.tax'].create({
'name': 'test_advanced_taxes_computation_0_4',
'amount_type': 'percent',
'amount': 10,
'sequence': 4,
})
tax_5 = self.env['account.tax'].create({
'name': 'test_advanced_taxes_computation_0_5',
'amount_type': 'percent',
'amount': 10,
'price_include': True,
'sequence': 5,
})
taxes = tax_1 + tax_2 + tax_3 + tax_4 + tax_5
res = taxes.compute_all(132.0)
self._check_compute_all_results(
154, # 'total_included'
100, # 'total_excluded'
[
# base , amount | seq | amount | incl | incl_base
# ---------------------------------------------------
(100.0, 10.0), # | 1 | 10% | t | t
(110.0, 11.0), # | 2 | 10% | |
(110.0, 11.0), # | 3 | 10% | t |
(110.0, 11.0), # | 4 | 10% | |
(110.0, 11.0), # | 5 | 10% | t |
# ---------------------------------------------------
],
res
)
@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import test_tax
@@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
from odoo.addons.account.tests.test_tax import TestTax
from odoo.tests import tagged
@tagged('post_install', '-at_install')
class TestTaxPython(TestTax):
def setUp(self):
super(TestTaxPython, self).setUp()
self.python_tax = self.tax_model.create({
'name': 'Python TAx',
'amount_type': 'code',
'amount': 0.0,
'python_compute': 'result = ((price_unit * quantity) - ((price_unit * quantity) / 1.12)) * 0.5',
'sequence': 1,
})
def test_tax_python_basic(self):
res = self.python_tax.compute_all(130.0)
self._check_compute_all_results(
136.96, # 'total_included'
130.0, # 'total_excluded'
[
# base , amount | seq | amount | incl | incl_base
# --------------------------------------------------
(130.0, 6.96), # | 1 | 6% | t |
# --------------------------------------------------
],
res
)
def test_tax_python_price_include(self):
self.python_tax.price_include = True
res = self.python_tax.compute_all(130.0)
self._check_compute_all_results(
130, # 'total_included'
123.04, # 'total_excluded'
[
# base , amount | seq | amount | incl | incl_base
# ---------------------------------------------------
(123.04, 6.96), # | 1 | 6% | t |
# ---------------------------------------------------
],
res
)
res = (self.python_tax + self.python_tax).compute_all(130.0)
self._check_compute_all_results(
130, # 'total_included'
116.07, # 'total_excluded'
[
# base , amount | seq | amount | incl | incl_base
# ---------------------------------------------------
(116.07, 6.96), # | 1 | 6% | t |
(116.07, 6.97), # | 1 | 6% | t |
# ---------------------------------------------------
],
res
)
+154 -62
View File
@@ -27,7 +27,7 @@ var exports = {};
// it must contains a representation of the server's PoS backend.
// (taxes, product list, configuration options, etc.) this representation
// is fetched and stored by the PosModel at the initialisation.
// this is done asynchronously, a ready promise alows the GUI to wait interactively
// this is done asynchronously, a ready deferred alows the GUI to wait interactively
// for the loading to be completed
// There is a single instance of the PosModel for each Front-End instance, it is usually called
// 'pos' and is available to all widgets extending PosWidget.
@@ -1714,88 +1714,179 @@ exports.Orderline = Backbone.Model.extend({
return taxes;
},
_compute_all: function(tax, base_amount, quantity) {
/**
* Mirror JS method of:
* _compute_amount in addons/account/models/account.py
*/
_compute_all: function(tax, base_amount, quantity, price_exclude) {
if(price_exclude === undefined)
var price_include = tax.price_include;
else
var price_include = !price_exclude;
if (tax.amount_type === 'fixed') {
var sign_base_amount = base_amount >= 0 ? 1 : -1;
return (Math.abs(tax.amount) * sign_base_amount) * quantity;
}
if ((tax.amount_type === 'percent' && !tax.price_include) || (tax.amount_type === 'division' && tax.price_include)){
if (tax.amount_type === 'percent' && !price_include){
return base_amount * tax.amount / 100;
}
if (tax.amount_type === 'percent' && tax.price_include){
if (tax.amount_type === 'percent' && price_include){
return base_amount - (base_amount / (1 + tax.amount / 100));
}
if (tax.amount_type === 'division' && !tax.price_include) {
if (tax.amount_type === 'division' && !price_include) {
return base_amount / (1 - tax.amount / 100) - base_amount;
}
if (tax.amount_type === 'division' && price_include) {
return base_amount - (base_amount * (tax.amount / 100));
}
return false;
},
compute_all: function(taxes, price_unit, quantity, currency_rounding, no_map_tax) {
/**
* Mirror JS method of:
* compute_all in addons/account/models/account.py
*
* Read comments in the python side method for more details about each sub-methods.
*/
compute_all: function(taxes, price_unit, quantity, currency_rounding) {
var self = this;
var list_taxes = [];
var currency_rounding_bak = currency_rounding;
if (this.pos.company.tax_calculation_rounding_method == "round_globally"){
currency_rounding = currency_rounding * 0.00001;
}
var total_excluded = round_pr(price_unit * quantity, currency_rounding);
var total_included = total_excluded;
var total_void = total_excluded;
var base = total_excluded;
var taxes_mapped = [];
if (!no_map_tax){
_(taxes).each(function(tax){
_(self._map_tax_fiscal_position(tax)).each(function(tax){
taxes_mapped.push(tax);
});
// 1) Flatten the taxes.
var _collect_taxes = function(taxes, all_taxes){
taxes.sort(function (tax1, tax2) {
return tax1.sequence - tax2.sequence;
});
} else {
taxes_mapped = taxes;
_(taxes).each(function(tax){
if(tax.amount_type === 'group')
all_taxes = _collect_taxes(tax.children_tax_ids, all_taxes);
else
all_taxes.push(tax);
});
return all_taxes;
}
var collect_taxes = function(taxes){
return _collect_taxes(taxes, []);
}
_(taxes_mapped).each(function(tax) {
if (tax.amount_type === 'group'){
var ret = self.compute_all(tax.children_tax_ids, price_unit, quantity, currency_rounding);
total_excluded = ret.total_excluded;
base = ret.total_excluded;
total_included = ret.total_included;
total_void = ret.total_void;
list_taxes = list_taxes.concat(ret.taxes);
}
else {
var tax_amount = self._compute_all(tax, base, quantity);
tax_amount = round_pr(tax_amount, currency_rounding);
if (tax_amount){
if (tax.price_include) {
total_excluded -= tax_amount;
base -= tax_amount;
}
else {
total_included += tax_amount;
}
if (tax.include_base_amount) {
base += tax_amount;
}
if (!tax.account_id) {
total_void += tax_amount;
}
var data = {
id: tax.id,
amount: tax_amount,
name: tax.name,
};
list_taxes.push(data);
taxes = collect_taxes(taxes);
// 2) Avoid dealing with taxes mixing price_include=False && include_base_amount=True
// with price_include=True
var base_excluded_flag = false; // price_include=False && include_base_amount=True
var included_flag = false; // price_include=True
_(taxes).each(function(tax){
if(tax.price_include)
included_flag = true;
else if(tax.include_base_amount)
base_excluded_flag = true;
if(base_excluded_flag && included_flag)
throw new Error('Unable to mix any taxes being price included with taxes affecting the base amount but not included in price.');
});
// 3) Deal with the rounding methods
var round_tax = this.pos.company.tax_calculation_rounding_method != 'round_globally';
if(!round_tax)
currency_rounding = currency_rounding * 0.00001;
// 4) Iterate the taxes in the reversed sequence order to retrieve the initial base of the computation.
var recompute_base = function(base_amount, fixed_amount, percent_amount, division_amount){
return (base_amount - fixed_amount) / (1.0 + percent_amount / 100.0) * (100 - division_amount) / 100;
}
var base = round_pr(price_unit * quantity, currency_rounding);
var sign = 1;
if(base < 0){
base = -base;
sign = -1;
}
var total_included_checkpoints = {};
var i = taxes.length - 1;
var store_included_tax_total = true;
var incl_fixed_amount = 0.0;
var incl_percent_amount = 0.0;
var incl_division_amount = 0.0;
var cached_tax_amounts = {};
_(taxes.reverse()).each(function(tax){
if(tax.include_base_amount){
base = recompute_base(base, incl_fixed_amount, incl_percent_amount, incl_division_amount);
incl_fixed_amount = 0.0;
incl_percent_amount = 0.0;
incl_division_amount = 0.0;
store_included_tax_total = true;
}
if(tax.price_include){
if(tax.amount_type === 'percent')
incl_percent_amount += tax.amount;
else if(tax.amount_type === 'division')
incl_division_amount += tax.amount;
else if(tax.amount_type === 'fixed')
incl_fixed_amount += quantity * tax.amount
else{
tax_amount = self._compute_all(tax, base, quantity);
incl_fixed_amount += tax_amount;
cached_tax_amounts[i] = tax_amount;
}
if(store_included_tax_total){
total_included_checkpoints[i] = base;
store_included_tax_total = false;
}
}
i -= 1;
});
var total_excluded = recompute_base(base, incl_fixed_amount, incl_percent_amount, incl_division_amount);
var total_included = total_excluded;
// 5) Iterate the taxes in the sequence order to fill missing base/amount values.
base = total_excluded;
var taxes_vals = [];
i = 0;
var cumulated_tax_included_amount = 0;
_(taxes.reverse()).each(function(tax){
if(tax.price_include && total_included_checkpoints[i] !== undefined){
var tax_amount = total_included_checkpoints[i] - (base + cumulated_tax_included_amount);
cumulated_tax_included_amount = 0;
}else
var tax_amount = self._compute_all(tax, base, quantity, true);
tax_amount = round_pr(tax_amount, currency_rounding);
if(tax.price_include && total_included_checkpoints[i] === undefined)
cumulated_tax_included_amount += tax_amount;
taxes_vals.push({
'id': tax.id,
'name': tax.name,
'amount': sign * tax_amount,
'base': sign * round_pr(base, currency_rounding),
});
if(tax.include_base_amount)
base += tax_amount;
total_included += tax_amount;
i += 1;
});
return {
taxes: list_taxes,
total_excluded: round_pr(total_excluded, currency_rounding_bak),
total_included: round_pr(total_included, currency_rounding_bak),
total_void: round_pr(total_void, currency_rounding_bak)
};
'taxes': taxes_vals,
'total_excluded': sign * round_pr(total_excluded, currency_rounding),
'total_included': sign * round_pr(total_included, currency_rounding),
}
},
get_all_prices: function(){
var self = this;
var price_unit = this.get_unit_price() * (1.0 - (this.get_discount() / 100.0));
var taxtotal = 0;
@@ -1806,9 +1897,10 @@ exports.Orderline = Backbone.Model.extend({
var product_taxes = [];
_(taxes_ids).each(function(el){
product_taxes.push(_.detect(taxes, function(t){
var tax = _.detect(taxes, function(t){
return t.id === el;
}));
});
product_taxes.push.apply(product_taxes, self._map_tax_fiscal_position(tax));
});
var all_taxes = this.compute_all(product_taxes, price_unit, this.get_quantity(), this.pos.currency.rounding);