[MERGE] forward port branch 9.0 up to ce818f1f8f
This commit is contained in:
@@ -4,7 +4,7 @@ import time
|
|||||||
import math
|
import math
|
||||||
|
|
||||||
from odoo.osv import expression
|
from odoo.osv import expression
|
||||||
from odoo.tools.float_utils import float_round as round, float_is_zero as is_zero
|
from odoo.tools.float_utils import float_round as round
|
||||||
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
|
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
|
||||||
from odoo.exceptions import UserError, ValidationError
|
from odoo.exceptions import UserError, ValidationError
|
||||||
from odoo import api, fields, models, _
|
from odoo import api, fields, models, _
|
||||||
@@ -682,7 +682,6 @@ class AccountTax(models.Model):
|
|||||||
price_unit * quantity eventually affected by previous taxes (if tax is include_base_amount XOR price_include)
|
price_unit * quantity eventually affected by previous taxes (if tax is include_base_amount XOR price_include)
|
||||||
"""
|
"""
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
price_include = self._context.get('force_price_include', self.price_include)
|
|
||||||
if self.amount_type == 'fixed':
|
if self.amount_type == 'fixed':
|
||||||
# Use copysign to take into account the sign of the base amount which includes the sign
|
# 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
|
# of the quantity and the sign of the price_unit
|
||||||
@@ -696,11 +695,11 @@ class AccountTax(models.Model):
|
|||||||
return math.copysign(quantity, base_amount) * self.amount
|
return math.copysign(quantity, base_amount) * self.amount
|
||||||
else:
|
else:
|
||||||
return quantity * self.amount
|
return quantity * self.amount
|
||||||
if (self.amount_type == 'percent' and not price_include) or (self.amount_type == 'division' and self.price_include):
|
if (self.amount_type == 'percent' and not self.price_include) or (self.amount_type == 'division' and self.price_include):
|
||||||
return base_amount * self.amount / 100
|
return base_amount * self.amount / 100
|
||||||
if self.amount_type == 'percent' and price_include:
|
if self.amount_type == 'percent' and self.price_include:
|
||||||
return base_amount - (base_amount / (1 + self.amount / 100))
|
return base_amount - (base_amount / (1 + self.amount / 100))
|
||||||
if self.amount_type == 'division' and not price_include:
|
if self.amount_type == 'division' and not self.price_include:
|
||||||
return base_amount / (1 - self.amount / 100) - base_amount
|
return base_amount / (1 - self.amount / 100) - base_amount
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
@@ -734,58 +733,13 @@ class AccountTax(models.Model):
|
|||||||
'analytic': boolean,
|
'analytic': boolean,
|
||||||
}]
|
}]
|
||||||
} """
|
} """
|
||||||
|
|
||||||
# 1) Flatten the taxes.
|
|
||||||
|
|
||||||
def collect_taxes(self, all_taxes=None):
|
|
||||||
# Collect all the taxes recursively ordered by the sequence.
|
|
||||||
# Example:
|
|
||||||
# group | seq | sub-group |
|
|
||||||
# ------------|-----------|
|
|
||||||
# | 1 | |
|
|
||||||
# ------------|-----------|
|
|
||||||
# t | 2 | | seq | |
|
|
||||||
# | | | 4 | |
|
|
||||||
# | | | 5 | |
|
|
||||||
# | | | 6 | |
|
|
||||||
# | | |
|
|
||||||
# ------------|-----------|
|
|
||||||
# | 3 | |
|
|
||||||
# ------------|-----------|
|
|
||||||
# Result: 1-4-5-6-3
|
|
||||||
if not all_taxes:
|
|
||||||
all_taxes = self.env['account.tax']
|
|
||||||
for tax in self.sorted(key=lambda r: r.sequence):
|
|
||||||
if tax.amount_type == 'group':
|
|
||||||
all_taxes = collect_taxes(tax.children_tax_ids, all_taxes)
|
|
||||||
else:
|
|
||||||
all_taxes += tax
|
|
||||||
return all_taxes
|
|
||||||
|
|
||||||
taxes = collect_taxes(self)
|
|
||||||
|
|
||||||
# 2) Avoid dealing with taxes mixing price_include=False && include_base_amount=True
|
|
||||||
# with price_include=True
|
|
||||||
|
|
||||||
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 len(self) == 0:
|
if len(self) == 0:
|
||||||
company_id = self.env.user.company_id
|
company_id = self.env.user.company_id
|
||||||
else:
|
else:
|
||||||
company_id = self[0].company_id
|
company_id = self[0].company_id
|
||||||
if not currency:
|
if not currency:
|
||||||
currency = company_id.currency_id
|
currency = company_id.currency_id
|
||||||
|
taxes = []
|
||||||
# By default, for each tax, tax amount will first be computed
|
# By default, for each tax, tax amount will first be computed
|
||||||
# and rounded at the 'Account' decimal precision for each
|
# and rounded at the 'Account' decimal precision for each
|
||||||
# PO/SO/invoice line and then these rounded amounts will be
|
# PO/SO/invoice line and then these rounded amounts will be
|
||||||
@@ -810,133 +764,50 @@ class AccountTax(models.Model):
|
|||||||
if not round_tax:
|
if not round_tax:
|
||||||
prec += 5
|
prec += 5
|
||||||
|
|
||||||
# 4) Iterate the taxes in the reversed sequence order to retrieve the initial base of the computation.
|
base_values = self.env.context.get('base_values')
|
||||||
# tax | base | amount |
|
if not base_values:
|
||||||
# /\ ----------------------------
|
total_excluded = total_included = base = round(price_unit * quantity, prec)
|
||||||
# || tax_1 | XXXX | | <- we are looking for that, it's the total_excluded
|
|
||||||
# || tax_2 | | |
|
|
||||||
# || tax_3 | | |
|
|
||||||
# || ... | .. | .. |
|
|
||||||
# ----------------------------
|
|
||||||
|
|
||||||
base = round(price_unit * quantity, prec)
|
|
||||||
|
|
||||||
# Keep track of subsequent recomputed bases in order to avoid some rounding issues.
|
|
||||||
# For example, 399.99 computed with a tax 20% price_include leads to
|
|
||||||
# base = 399.99 / 1.2 = 333.32500000000005
|
|
||||||
# tax_amount = base * 0.2 = 66.665
|
|
||||||
# round(base) + round(tax_amount) = 333.33 + 66.67 = 400.0 (!= 399.99: WRONG)
|
|
||||||
#
|
|
||||||
# To fix such issues, base_gaps will contains amount between two bases.
|
|
||||||
# In our example, the gap between 333.32500000000005 and 399.99 is 66.66499999999996
|
|
||||||
#
|
|
||||||
# Then, when processing the tax and because 66.665 - 66.66499999999996 is close to zero,
|
|
||||||
# the real gap is returned and so:
|
|
||||||
# tax_amount = 66.66499999999996
|
|
||||||
# round(base) + round(tax_amount) = 333.33 + 66.66 = 399.99 (CORRECT)
|
|
||||||
base_gaps = []
|
|
||||||
|
|
||||||
def recompute_base(base_amount, fixed_amount, percent_amount):
|
|
||||||
# Recompute the new base amount based on included fixed/percent amount and the current base amount.
|
|
||||||
# Example:
|
|
||||||
# tax | amount |
|
|
||||||
# ------------------
|
|
||||||
# tax_1 | 10% |
|
|
||||||
# tax_2 | 15 |
|
|
||||||
# tax_3 | 20% |
|
|
||||||
# ------------------
|
|
||||||
# if base_amount = 145, the new base is computed as:
|
|
||||||
# (145 - 15) / (1.0 + ((10 + 20) / 100.0)) = 130 / 1.3 = 100
|
|
||||||
if fixed_amount == 0.0 and percent_amount == 0.0:
|
|
||||||
return base_amount
|
|
||||||
new_base = (base_amount - fixed_amount) / (1.0 + percent_amount / 100.0)
|
|
||||||
base_gaps.append(base_amount - new_base)
|
|
||||||
return new_base
|
|
||||||
|
|
||||||
# For the computation of move lines, we could have a negative base value.
|
|
||||||
# In this case, compute all with positive values and negative them at the end.
|
|
||||||
if base < 0:
|
|
||||||
base = -base
|
|
||||||
sign = -1
|
|
||||||
else:
|
else:
|
||||||
sign = 1
|
total_excluded, total_included, base = base_values
|
||||||
|
|
||||||
# Keep track of the accumulated included fixed/percent amount.
|
# Sorting key is mandatory in this case. When no key is provided, sorted() will perform a
|
||||||
incl_fixed_amount = incl_percent_amount = 0
|
# search. However, the search method is overridden in account.tax in order to add a domain
|
||||||
for tax in reversed(taxes):
|
# depending on the context. This domain might filter out some taxes from self, e.g. in the
|
||||||
if tax.include_base_amount:
|
# case of group taxes.
|
||||||
base = recompute_base(base, incl_fixed_amount, incl_percent_amount)
|
for tax in self.sorted(key=lambda r: r.sequence):
|
||||||
incl_fixed_amount = incl_percent_amount = 0
|
if tax.amount_type == 'group':
|
||||||
if tax.price_include:
|
children = tax.children_tax_ids.with_context(base_values=(total_excluded, total_included, base))
|
||||||
if tax.amount_type == 'fixed':
|
ret = children.compute_all(price_unit, currency, quantity, product, partner)
|
||||||
incl_fixed_amount += quantity * tax.amount
|
total_excluded = ret['total_excluded']
|
||||||
elif tax.amount_type == 'percent':
|
base = ret['base'] if tax.include_base_amount else base
|
||||||
incl_percent_amount += tax.amount
|
total_included = ret['total_included']
|
||||||
# Start the computation of accumulated amounts at the total_excluded value.
|
tax_amount = total_included - total_excluded
|
||||||
total_excluded = total_included = base = recompute_base(base, incl_fixed_amount, incl_percent_amount)
|
taxes += ret['taxes']
|
||||||
|
continue
|
||||||
|
|
||||||
# 5) Iterate the taxes in the sequence order to fill missing base/amount values.
|
tax_amount = tax._compute_amount(base, price_unit, quantity, product, partner)
|
||||||
# tax | base | amount |
|
|
||||||
# || ----------------------------
|
|
||||||
# || tax_1 | OK | XXXX |
|
|
||||||
# || tax_2 | XXXX | XXXX |
|
|
||||||
# || tax_3 | XXXX | XXXX |
|
|
||||||
# \/ ... | .. | .. |
|
|
||||||
# ----------------------------
|
|
||||||
|
|
||||||
def compute_amount(tax):
|
|
||||||
# Compute the amount of the tax but don't deal with the price_include because it's already
|
|
||||||
# took into account on the base amount except for 'division' tax:
|
|
||||||
# (tax.amount_type == 'percent' && not tax.price_include)
|
|
||||||
# == (tax.amount_type == 'division' && tax.price_include)
|
|
||||||
#
|
|
||||||
# In case of price_included tax, subtract the amount to the corresponding
|
|
||||||
# gap between the current base and the next one.
|
|
||||||
amount = tax.with_context(force_price_include=False)._compute_amount(
|
|
||||||
base, price_unit, quantity, product, partner)
|
|
||||||
|
|
||||||
if not tax.price_include or not base_gaps:
|
|
||||||
return amount
|
|
||||||
|
|
||||||
# Compute the new gap after subtracting of the tax amount
|
|
||||||
new_gap = base_gaps[-1] - amount
|
|
||||||
|
|
||||||
# If the newly computed gap is very close of zero, return the current gap to avoid
|
|
||||||
# rounding issues (see comments above base_gaps).
|
|
||||||
if is_zero(new_gap, prec):
|
|
||||||
return base_gaps.pop()
|
|
||||||
|
|
||||||
# Update the current gap with the new one
|
|
||||||
base_gaps[-1] = new_gap
|
|
||||||
return amount
|
|
||||||
|
|
||||||
taxes_vals = []
|
|
||||||
for tax in taxes:
|
|
||||||
tax_amount = compute_amount(tax)
|
|
||||||
if not round_tax:
|
if not round_tax:
|
||||||
tax_amount = round(tax_amount, prec)
|
tax_amount = round(tax_amount, prec)
|
||||||
else:
|
else:
|
||||||
tax_amount = currency.round(tax_amount)
|
tax_amount = currency.round(tax_amount)
|
||||||
|
|
||||||
# Suppose:
|
if tax.price_include:
|
||||||
# seq | amount | incl | incl_base | base | amount
|
total_excluded -= tax_amount
|
||||||
# -----------------------------------------------
|
base -= tax_amount
|
||||||
# 1 | 10 % | t | t | 100.0 | 10.0
|
else:
|
||||||
# -----------------------------------------------
|
total_included += tax_amount
|
||||||
# ... the next computation must be done using 100.0 + 10.0 = 110.0 as base but
|
|
||||||
# the tax base of this tax will be 100.0.
|
# Keep base amount used for the current tax
|
||||||
tax_base = base
|
tax_base = base
|
||||||
|
|
||||||
if tax.include_base_amount:
|
if tax.include_base_amount:
|
||||||
base += tax_amount
|
base += tax_amount
|
||||||
|
|
||||||
# The total_included amount is computed as the sum of total_excluded with all tax_amount
|
taxes.append({
|
||||||
total_included += tax_amount
|
|
||||||
|
|
||||||
taxes_vals.append({
|
|
||||||
'id': tax.id,
|
'id': tax.id,
|
||||||
'name': tax.with_context(**{'lang': partner.lang} if partner else {}).name,
|
'name': tax.with_context(**{'lang': partner.lang} if partner else {}).name,
|
||||||
'amount': sign * tax_amount,
|
'amount': tax_amount,
|
||||||
'base': round(sign * tax_base, prec),
|
'base': tax_base,
|
||||||
'sequence': tax.sequence,
|
'sequence': tax.sequence,
|
||||||
'account_id': tax.account_id.id,
|
'account_id': tax.account_id.id,
|
||||||
'refund_account_id': tax.refund_account_id.id,
|
'refund_account_id': tax.refund_account_id.id,
|
||||||
@@ -944,10 +815,10 @@ class AccountTax(models.Model):
|
|||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'taxes': taxes_vals,
|
'taxes': sorted(taxes, key=lambda k: k['sequence']),
|
||||||
'total_excluded': sign * (currency.round(total_excluded) if round_total else total_excluded),
|
'total_excluded': currency.round(total_excluded) if round_total else total_excluded,
|
||||||
'total_included': sign * (currency.round(total_included) if round_total else total_included),
|
'total_included': currency.round(total_included) if round_total else total_included,
|
||||||
'base': round(sign * base, prec),
|
'base': base,
|
||||||
}
|
}
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from odoo.addons.account.tests.account_test_users import AccountTestUsers
|
from odoo.addons.account.tests.account_test_users import AccountTestUsers
|
||||||
|
from openerp.tests.common import TransactionCase
|
||||||
|
from openerp.tools import float_compare
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
|
||||||
@@ -28,12 +27,6 @@ class TestTax(AccountTestUsers):
|
|||||||
'amount': 10,
|
'amount': 10,
|
||||||
'sequence': 3,
|
'sequence': 3,
|
||||||
})
|
})
|
||||||
self.percent_tax_bis = self.tax_model.create({
|
|
||||||
'name': "Percent tax bis",
|
|
||||||
'amount_type': 'percent',
|
|
||||||
'amount': 21,
|
|
||||||
'sequence': 3,
|
|
||||||
})
|
|
||||||
self.division_tax = self.tax_model.create({
|
self.division_tax = self.tax_model.create({
|
||||||
'name': "Division tax",
|
'name': "Division tax",
|
||||||
'amount_type': 'division',
|
'amount_type': 'division',
|
||||||
@@ -74,125 +67,47 @@ class TestTax(AccountTestUsers):
|
|||||||
self.bank_account = self.bank_journal.default_debit_account_id
|
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
|
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, base, total_included, total_excluded, taxes, res):
|
|
||||||
self.assertAlmostEqual(res['base'], base)
|
|
||||||
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):
|
def test_tax_group_of_group_tax(self):
|
||||||
self.fixed_tax.include_base_amount = True
|
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)
|
res = self.group_of_group_tax.compute_all(200.0)
|
||||||
self._check_compute_all_results(
|
self.assertEquals(res['total_excluded'], 200.0)
|
||||||
220, # 'base'
|
# After calculation of first group
|
||||||
263, # 'total_included'
|
# base = 210
|
||||||
200, # 'total_excluded'
|
# total_included = 231
|
||||||
[
|
# Base of the first grouped is passed
|
||||||
# base , amount | seq | amount | incl | incl_base
|
# Base after the second group (220) is dropped.
|
||||||
# ---------------------------------------------------
|
# Base of the group of groups is passed out,
|
||||||
(200.0, 10.0), # | 1 | 10 | | t
|
# so we obtain base as after first group
|
||||||
(210.0, 21.0), # | 3 | 10% | |
|
self.assertEquals(res['base'], 210.0)
|
||||||
(210.0, 10.0), # | 1 | 10 | | t
|
self.assertEquals(res['total_included'], 263.0)
|
||||||
(220.0, 22.0), # | 3 | 10% | |
|
|
||||||
# ---------------------------------------------------
|
|
||||||
],
|
|
||||||
res
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_tax_group(self):
|
def test_tax_group(self):
|
||||||
res = self.group_tax.compute_all(200.0)
|
res = self.group_tax.compute_all(200.0)
|
||||||
self._check_compute_all_results(
|
self.assertEquals(res['total_excluded'], 200.0)
|
||||||
200, # 'base'
|
self.assertEquals(res['total_included'], 230.0)
|
||||||
230, # 'total_included'
|
self.assertEquals(len(res['taxes']), 2)
|
||||||
200, # 'total_excluded'
|
self.assertEquals(res['taxes'][0]['amount'], 10.0)
|
||||||
[
|
self.assertEquals(res['taxes'][1]['amount'], 20.0)
|
||||||
# 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):
|
def test_tax_percent_division(self):
|
||||||
self.division_tax.price_include = True
|
self.division_tax.price_include = True
|
||||||
self.division_tax.include_base_amount = True
|
self.division_tax.include_base_amount = True
|
||||||
res_division = self.division_tax.compute_all(200.0)
|
|
||||||
self._check_compute_all_results(
|
|
||||||
220, # 'base'
|
|
||||||
220, # 'total_included'
|
|
||||||
200, # 'total_excluded'
|
|
||||||
[
|
|
||||||
# base , amount | seq | amount | incl | incl_base
|
|
||||||
# ---------------------------------------------------
|
|
||||||
(200.0, 20.0), # | 4 | 10/ | t | t
|
|
||||||
# ---------------------------------------------------
|
|
||||||
],
|
|
||||||
res_division
|
|
||||||
)
|
|
||||||
self.percent_tax.price_include = False
|
self.percent_tax.price_include = False
|
||||||
self.percent_tax.include_base_amount = 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)
|
res_percent = self.percent_tax.compute_all(200.0)
|
||||||
self._check_compute_all_results(
|
self.assertEquals(res_division['taxes'][0]['amount'], 20.0)
|
||||||
200, # 'base'
|
self.assertEquals(res_percent['taxes'][0]['amount'], 20.0)
|
||||||
220, # 'total_included'
|
|
||||||
200, # 'total_excluded'
|
|
||||||
[
|
|
||||||
# base , amount | seq | amount | incl | incl_base
|
|
||||||
# ---------------------------------------------------
|
|
||||||
(200.0, 20.0), # | 3 | 10% | |
|
|
||||||
# ---------------------------------------------------
|
|
||||||
],
|
|
||||||
res_percent
|
|
||||||
)
|
|
||||||
self.division_tax.price_include = False
|
self.division_tax.price_include = False
|
||||||
self.division_tax.include_base_amount = False
|
self.division_tax.include_base_amount = False
|
||||||
res_division = self.division_tax.compute_all(200.0)
|
|
||||||
self._check_compute_all_results(
|
|
||||||
200, # 'base'
|
|
||||||
222.22, # 'total_included'
|
|
||||||
200, # 'total_excluded'
|
|
||||||
[
|
|
||||||
# base , amount | seq | amount | incl | incl_base
|
|
||||||
# ---------------------------------------------------
|
|
||||||
(200.0, 22.22), # | 4 | 10/ | |
|
|
||||||
# ---------------------------------------------------
|
|
||||||
],
|
|
||||||
res_division
|
|
||||||
)
|
|
||||||
self.percent_tax.price_include = True
|
self.percent_tax.price_include = True
|
||||||
self.percent_tax.include_base_amount = 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)
|
res_percent = self.percent_tax.compute_all(200.0)
|
||||||
self._check_compute_all_results(
|
self.assertEquals(res_division['taxes'][0]['amount'], 22.22)
|
||||||
200, # 'base'
|
self.assertEquals(res_percent['taxes'][0]['amount'], 18.18)
|
||||||
200, # 'total_included'
|
|
||||||
181.82, # 'total_excluded'
|
|
||||||
[
|
|
||||||
# base , amount | seq | amount | incl | incl_base
|
|
||||||
# ---------------------------------------------------
|
|
||||||
(181.82, 18.18), # | 3 | 10% | t | t
|
|
||||||
# ---------------------------------------------------
|
|
||||||
],
|
|
||||||
res_percent
|
|
||||||
)
|
|
||||||
self.percent_tax_bis.price_include = True
|
|
||||||
self.percent_tax_bis.include_base_amount = True
|
|
||||||
res_percent = self.percent_tax_bis.compute_all(7.0)
|
|
||||||
self._check_compute_all_results(
|
|
||||||
7.0, # 'base'
|
|
||||||
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):
|
def test_tax_sequence_normalized_set(self):
|
||||||
self.division_tax.sequence = 1
|
self.division_tax.sequence = 1
|
||||||
@@ -200,103 +115,19 @@ class TestTax(AccountTestUsers):
|
|||||||
self.percent_tax.sequence = 3
|
self.percent_tax.sequence = 3
|
||||||
taxes_set = (self.group_tax | self.division_tax)
|
taxes_set = (self.group_tax | self.division_tax)
|
||||||
res = taxes_set.compute_all(200.0)
|
res = taxes_set.compute_all(200.0)
|
||||||
self._check_compute_all_results(
|
self.assertEquals(res['taxes'][0]['amount'], 22.22)
|
||||||
200, # 'base'
|
self.assertEquals(res['taxes'][1]['amount'], 10.0)
|
||||||
252.22, # 'total_included'
|
self.assertEquals(res['taxes'][2]['amount'], 20.0)
|
||||||
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_tax_include_base_amount(self):
|
||||||
self.fixed_tax.include_base_amount = True
|
self.fixed_tax.include_base_amount = True
|
||||||
res = self.group_tax.compute_all(200.0)
|
res = self.group_tax.compute_all(200.0)
|
||||||
self._check_compute_all_results(
|
self.assertEquals(res['total_included'], 231.0)
|
||||||
210, # 'base'
|
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
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(
|
|
||||||
180, # 'base'
|
|
||||||
200, # 'total_included'
|
|
||||||
180, # 'total_excluded'
|
|
||||||
[
|
|
||||||
# base , amount | seq | amount | incl | incl_base
|
|
||||||
# ---------------------------------------------------
|
|
||||||
(180.0, 20.0), # | 1 | 20 | | t
|
|
||||||
# ---------------------------------------------------
|
|
||||||
],
|
|
||||||
res
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_tax_include_base_amount_2(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(
|
|
||||||
5.79, # 'base'
|
|
||||||
7.0, # 'total_included'
|
|
||||||
5.79, # 'total_excluded'
|
|
||||||
[
|
|
||||||
# base , amount | seq | amount | incl | incl_base
|
|
||||||
# ---------------------------------------------------
|
|
||||||
(5.79, 1.21), # | 3 | 10% | t |
|
|
||||||
# ---------------------------------------------------
|
|
||||||
],
|
|
||||||
res
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_tax_include_base_amount_3(self):
|
|
||||||
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(
|
|
||||||
333.33, # 'base'
|
|
||||||
399.99, # 'total_included'
|
|
||||||
333.33, # 'total_excluded'
|
|
||||||
[
|
|
||||||
# base , amount | seq | amount | incl | incl_base
|
|
||||||
# ---------------------------------------------------
|
|
||||||
(333.33, 66.66), # | 3 | 10% | t |
|
|
||||||
# ---------------------------------------------------
|
|
||||||
],
|
|
||||||
res
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_tax_currency(self):
|
def test_tax_currency(self):
|
||||||
self.division_tax.amount = 15.0
|
self.division_tax.amount = 15.0
|
||||||
res = self.division_tax.compute_all(200.0, currency=self.env.ref('base.VEF'))
|
res = self.division_tax.compute_all(200.0, currency=self.env.ref('base.VEF'))
|
||||||
self._check_compute_all_results(
|
self.assertAlmostEqual(res['total_included'], 235.2941)
|
||||||
200, # 'base'
|
|
||||||
235.2941, # 'total_included'
|
|
||||||
200, # 'total_excluded'
|
|
||||||
[
|
|
||||||
# base , amount | seq | amount | incl | incl_base
|
|
||||||
# ---------------------------------------------------
|
|
||||||
(200.0, 35.2941), # | 4 | 15/ | |
|
|
||||||
# ---------------------------------------------------
|
|
||||||
],
|
|
||||||
res
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_tax_move_lines_creation(self):
|
def test_tax_move_lines_creation(self):
|
||||||
""" Test that creating a move.line with tax_ids generates the tax move lines and adjust line amount when a tax is price_include """
|
""" Test that creating a move.line with tax_ids generates the tax move lines and adjust line amount when a tax is price_include """
|
||||||
@@ -341,58 +172,3 @@ class TestTax(AccountTestUsers):
|
|||||||
aml_with_taxes = move.line_ids.filtered(lambda l: set(l.tax_ids.ids) == set([self.group_tax.id, self.fixed_tax_bis.id]))
|
aml_with_taxes = move.line_ids.filtered(lambda l: set(l.tax_ids.ids) == set([self.group_tax.id, self.fixed_tax_bis.id]))
|
||||||
self.assertEquals(len(aml_with_taxes), 1)
|
self.assertEquals(len(aml_with_taxes), 1)
|
||||||
self.assertEquals(aml_with_taxes.credit, 190)
|
self.assertEquals(aml_with_taxes.credit, 190)
|
||||||
|
|
||||||
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(
|
|
||||||
110, # 'base'
|
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ exports.PosModel = Backbone.Model.extend({
|
|||||||
},
|
},
|
||||||
},{
|
},{
|
||||||
model: 'account.tax',
|
model: 'account.tax',
|
||||||
fields: ['name','amount', 'price_include', 'include_base_amount', 'amount_type', 'children_tax_ids', 'sequence'],
|
fields: ['name','amount', 'price_include', 'include_base_amount', 'amount_type', 'children_tax_ids'],
|
||||||
domain: null,
|
domain: null,
|
||||||
loaded: function(self, taxes){
|
loaded: function(self, taxes){
|
||||||
self.taxes = taxes;
|
self.taxes = taxes;
|
||||||
@@ -1400,159 +1400,74 @@ exports.Orderline = Backbone.Model.extend({
|
|||||||
|
|
||||||
return tax;
|
return tax;
|
||||||
},
|
},
|
||||||
/**
|
_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_include) {
|
|
||||||
if(price_include === undefined)
|
|
||||||
price_include = tax.price_include;
|
|
||||||
if (tax.amount_type === 'fixed') {
|
if (tax.amount_type === 'fixed') {
|
||||||
var sign_base_amount = base_amount >= 0 ? 1 : -1;
|
var sign_base_amount = base_amount >= 0 ? 1 : -1;
|
||||||
return (Math.abs(tax.amount) * sign_base_amount) * quantity;
|
return (Math.abs(tax.amount) * sign_base_amount) * quantity;
|
||||||
}
|
}
|
||||||
if ((tax.amount_type === 'percent' && !price_include) || (tax.amount_type === 'division' && tax.price_include)){
|
if ((tax.amount_type === 'percent' && !tax.price_include) || (tax.amount_type === 'division' && tax.price_include)){
|
||||||
return base_amount * tax.amount / 100;
|
return base_amount * tax.amount / 100;
|
||||||
}
|
}
|
||||||
if (tax.amount_type === 'percent' && price_include){
|
if (tax.amount_type === 'percent' && tax.price_include){
|
||||||
return base_amount - (base_amount / (1 + tax.amount / 100));
|
return base_amount - (base_amount / (1 + tax.amount / 100));
|
||||||
}
|
}
|
||||||
if (tax.amount_type === 'division' && !price_include) {
|
if (tax.amount_type === 'division' && !tax.price_include) {
|
||||||
return base_amount / (1 - tax.amount / 100) - base_amount;
|
return base_amount / (1 - tax.amount / 100) - base_amount;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
/**
|
|
||||||
* 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, no_map_tax) {
|
compute_all: function(taxes, price_unit, quantity, currency_rounding, no_map_tax) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
var list_taxes = [];
|
||||||
// 1) Flatten the taxes.
|
|
||||||
|
|
||||||
var _collect_taxes = function(taxes, all_taxes){
|
|
||||||
taxes.sort(function (tax1, tax2) {
|
|
||||||
return tax1.sequence - tax2.sequence;
|
|
||||||
});
|
|
||||||
_(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 = 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 currency_rounding_bak = currency_rounding;
|
var currency_rounding_bak = currency_rounding;
|
||||||
if (this.pos.company.tax_calculation_rounding_method == "round_globally"){
|
if (this.pos.company.tax_calculation_rounding_method == "round_globally"){
|
||||||
currency_rounding = currency_rounding * 0.00001;
|
currency_rounding = currency_rounding * 0.00001;
|
||||||
}
|
}
|
||||||
|
var total_excluded = round_pr(price_unit * quantity, currency_rounding);
|
||||||
// 4) Iterate the taxes in the reversed sequence order to retrieve the initial base of the computation.
|
var total_included = total_excluded;
|
||||||
|
var base = total_excluded;
|
||||||
var base = round_pr(price_unit * quantity, currency_rounding);
|
_(taxes).each(function(tax) {
|
||||||
|
if (!no_map_tax){
|
||||||
var base_gaps = [];
|
tax = self._map_tax_fiscal_position(tax);
|
||||||
|
|
||||||
var recompute_base = function(base_amount, fixed_amount, percent_amount){
|
|
||||||
if(fixed_amount === 0.0 && percent_amount === 0.0)
|
|
||||||
return base_amount;
|
|
||||||
var new_base = (base_amount - fixed_amount) / (1.0 + percent_amount / 100.0);
|
|
||||||
base_gaps.push(base_amount - new_base);
|
|
||||||
return new_base;
|
|
||||||
}
|
|
||||||
|
|
||||||
var sign = 1;
|
|
||||||
if(base < 0){
|
|
||||||
base = -base;
|
|
||||||
sign = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
var incl_fixed_amount = 0.0;
|
|
||||||
var incl_percent_amount = 0.0;
|
|
||||||
_(taxes.reverse()).each(function(tax){
|
|
||||||
if(tax.include_base_amount){
|
|
||||||
base = recompute_base(base, incl_fixed_amount, incl_percent_amount);
|
|
||||||
incl_fixed_amount = 0.0;
|
|
||||||
incl_percent_amount = 0.0;
|
|
||||||
}
|
}
|
||||||
if(tax.price_include){
|
if (!tax){
|
||||||
if(tax.amount_type === 'fixed')
|
return;
|
||||||
incl_fixed_amount += quantity * tax.amount;
|
|
||||||
else if(tax.amount_type === 'percent')
|
|
||||||
incl_percent_amount += tax.amount;
|
|
||||||
}
|
}
|
||||||
});
|
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;
|
||||||
|
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);
|
||||||
|
|
||||||
base = recompute_base(base, incl_fixed_amount, incl_percent_amount);
|
if (tax_amount){
|
||||||
var total_excluded = base;
|
if (tax.price_include) {
|
||||||
var total_included = base;
|
total_excluded -= tax_amount;
|
||||||
|
base -= tax_amount;
|
||||||
// 5) Iterate the taxes in the sequence order to fill missing base/amount values.
|
}
|
||||||
|
else {
|
||||||
var compute_amount = function(tax){
|
total_included += tax_amount;
|
||||||
var amount = self._compute_all(tax, base, quantity, false);
|
}
|
||||||
|
if (tax.include_base_amount) {
|
||||||
if(!tax.price_include || base_gaps.length == 0)
|
base += tax_amount;
|
||||||
return amount;
|
}
|
||||||
|
var data = {
|
||||||
var new_gap = base_gaps[base_gaps.length - 1] - amount;
|
id: tax.id,
|
||||||
|
amount: tax_amount,
|
||||||
if(round_pr(new_gap, currency_rounding) === 0.0)
|
name: tax.name,
|
||||||
return base_gaps.pop();
|
};
|
||||||
|
list_taxes.push(data);
|
||||||
base_gaps[base_gaps.length - 1] = new_gap;
|
}
|
||||||
return amount;
|
}
|
||||||
};
|
|
||||||
|
|
||||||
var taxes_vals = [];
|
|
||||||
_(taxes.reverse()).each(function(tax){
|
|
||||||
var tax_amount = compute_amount(tax);
|
|
||||||
tax_amount = round_pr(tax_amount, currency_rounding);
|
|
||||||
|
|
||||||
var tax_base = base;
|
|
||||||
|
|
||||||
if(tax.include_base_amount)
|
|
||||||
base += tax_amount;
|
|
||||||
|
|
||||||
total_included += tax_amount;
|
|
||||||
|
|
||||||
var tax_vals = {
|
|
||||||
id: tax.id,
|
|
||||||
amount: sign * tax_amount,
|
|
||||||
name: tax.name,
|
|
||||||
};
|
|
||||||
|
|
||||||
taxes_vals.push(tax_vals);
|
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
taxes: taxes_vals,
|
taxes: list_taxes,
|
||||||
total_excluded: sign * round_pr(total_excluded, currency_rounding_bak),
|
total_excluded: round_pr(total_excluded, currency_rounding_bak),
|
||||||
total_included: sign * round_pr(total_included, currency_rounding_bak)
|
total_included: round_pr(total_included, currency_rounding_bak)
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
get_all_prices: function(){
|
get_all_prices: function(){
|
||||||
@@ -1570,7 +1485,6 @@ exports.Orderline = Backbone.Model.extend({
|
|||||||
return t.id === el;
|
return t.id === el;
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
product_taxes = _.map(product_taxes, this._map_tax_fiscal_position.bind(this));
|
|
||||||
|
|
||||||
var all_taxes = this.compute_all(product_taxes, price_unit, this.get_quantity(), this.pos.currency.rounding);
|
var all_taxes = this.compute_all(product_taxes, price_unit, this.get_quantity(), this.pos.currency.rounding);
|
||||||
_(all_taxes.taxes).each(function(tax) {
|
_(all_taxes.taxes).each(function(tax) {
|
||||||
|
|||||||
Reference in New Issue
Block a user